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.
This commit is contained in:
2026-03-04 20:52:02 +01:00
parent f4621b3425
commit 49e8e066d7
16 changed files with 1732 additions and 0 deletions

165
sdks/python/README.md Normal file
View File

@@ -0,0 +1,165 @@
# quicproquo Python SDK
Python client library for the [quicproquo](https://github.com/nicholasgasior/quicproquo) E2E encrypted messenger.
## Prerequisites
- Python 3.10+
- A running quicproquo server
## Installation
```sh
pip install quicproquo
```
For development:
```sh
pip install -e ".[dev]"
```
## Transport Backends
### 1. Async QUIC (pure Python)
Uses [aioquic](https://github.com/aiortc/aioquic) for native QUIC transport with the v2 protobuf wire format. No Rust dependency required.
```python
import asyncio
from quicproquo import QpqClient, ConnectOptions
async def main():
client = await QpqClient.connect(ConnectOptions(
addr="127.0.0.1:5001",
ca_cert_path="ca.pem",
))
health = await client.health()
print(f"Server: {health.status}")
# OPAQUE auth (requires external OPAQUE library for crypto)
server_resp = await client.login_start("alice", opaque_request_bytes)
# ... process server_resp with OPAQUE library ...
token = await client.login_finish("alice", finalization, identity_key)
# Resolve a user
key, proof = await client.resolve_user("bob")
# Send a message
seq, proof = await client.send(recipient_key, b"hello")
# Receive messages (long-poll)
messages = await client.receive_wait(my_key, timeout_ms=5000)
for msg in messages:
print(f"[{msg.seq}] {msg.data}")
await client.close()
asyncio.run(main())
```
### 2. Rust FFI (synchronous)
Wraps `libquicproquo_ffi` via CFFI for full Rust crypto stack (MLS, hybrid KEM, OPAQUE) at native speed.
```sh
# Build the FFI library first
cargo build --release -p quicproquo-ffi
```
```python
from quicproquo import QpqClient, ConnectOptions
client = QpqClient.connect_ffi(ConnectOptions(
addr="127.0.0.1:5001",
ca_cert_path="ca.pem",
))
client.ffi_login("alice", "password123")
client.ffi_send("bob", b"hello from Python!")
messages = client.ffi_receive(timeout_ms=5000)
for msg in messages:
print(msg)
client.close_sync()
```
## API Reference
### Connection
| Method | Transport | Description |
|---|---|---|
| `QpqClient.connect(opts)` | QUIC | Async connect to server |
| `QpqClient.connect_ffi(opts)` | FFI | Sync connect via Rust FFI |
| `client.close()` | QUIC | Async disconnect |
| `client.close_sync()` | Both | Sync disconnect |
### Authentication (QUIC)
| Method | Description |
|---|---|
| `client.register_start(username, request)` | Start OPAQUE registration |
| `client.register_finish(username, upload, identity_key)` | Complete registration |
| `client.login_start(username, request)` | Start OPAQUE login |
| `client.login_finish(username, finalization, identity_key)` | Complete login |
| `client.set_session_token(token)` | Set pre-existing session token |
### Authentication (FFI)
| Method | Description |
|---|---|
| `client.ffi_login(username, password)` | Full OPAQUE login (Rust handles crypto) |
### Messaging (QUIC)
| Method | Description |
|---|---|
| `client.health()` | Server health check |
| `client.resolve_user(username)` | Look up identity key |
| `client.resolve_identity(key)` | Reverse look up username |
| `client.create_channel(peer_key)` | Create 1:1 DM channel |
| `client.send(recipient_key, payload)` | Send a message |
| `client.receive(recipient_key)` | Fetch queued messages |
| `client.receive_wait(recipient_key, timeout_ms=5000)` | Long-poll for messages |
| `client.ack(recipient_key, seq_up_to)` | Acknowledge messages |
| `client.upload_key_package(key, package)` | Upload MLS key package |
| `client.fetch_key_package(key)` | Fetch MLS key package |
| `client.delete_account()` | Permanently delete account |
### Messaging (FFI)
| Method | Description |
|---|---|
| `client.ffi_send(recipient, message)` | Send message by username |
| `client.ffi_receive(timeout_ms=5000)` | Receive pending messages |
## Wire Format
The SDK implements the qpq v2 wire format:
```
[method_id:u16][req_id:u32][len:u32][protobuf payload]
```
Each RPC is sent over its own QUIC bidirectional stream.
## Structure
- `quicproquo/client.py` -- High-level client API
- `quicproquo/transport.py` -- QUIC transport (aioquic)
- `quicproquo/ffi.py` -- Rust FFI transport (CFFI)
- `quicproquo/proto.py` -- Protobuf encode/decode (no codegen)
- `quicproquo/wire.py` -- v2 wire format framing
- `quicproquo/types.py` -- Data types and exceptions
- `examples/bot.py` -- Async echo bot example
- `examples/ffi_demo.py` -- Synchronous FFI example
## Running Tests
```sh
pip install -e ".[dev]"
pytest
```