chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
This commit is contained in:
231
examples/python/qpc_client.py
Normal file
231
examples/python/qpc_client.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""quicprochat Python client -- ctypes wrapper around libquicprochat_ffi.
|
||||
|
||||
Usage:
|
||||
python qpq_client.py --server 127.0.0.1:7000 \\
|
||||
--ca-cert server-cert.der \\
|
||||
--server-name localhost \\
|
||||
--username alice --password secret \\
|
||||
[--send-to bob --message "hello"] \\
|
||||
[--receive --timeout 5000]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from ctypes import (
|
||||
POINTER,
|
||||
c_char_p,
|
||||
c_int,
|
||||
c_size_t,
|
||||
c_uint32,
|
||||
c_void_p,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status codes (must match quicprochat-ffi/src/lib.rs)
|
||||
# ---------------------------------------------------------------------------
|
||||
QPQ_OK = 0
|
||||
QPQ_ERROR = 1
|
||||
QPQ_AUTH_FAILED = 2
|
||||
QPQ_TIMEOUT = 3
|
||||
QPQ_NOT_CONNECTED = 4
|
||||
|
||||
STATUS_NAMES = {
|
||||
QPQ_OK: "OK",
|
||||
QPQ_ERROR: "ERROR",
|
||||
QPQ_AUTH_FAILED: "AUTH_FAILED",
|
||||
QPQ_TIMEOUT: "TIMEOUT",
|
||||
QPQ_NOT_CONNECTED: "NOT_CONNECTED",
|
||||
}
|
||||
|
||||
|
||||
def _status_name(code: int) -> str:
|
||||
return STATUS_NAMES.get(code, f"UNKNOWN({code})")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Library loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _find_library() -> str:
|
||||
"""Locate libquicprochat_ffi shared library."""
|
||||
env = os.environ.get("QPQ_FFI_LIB")
|
||||
if env:
|
||||
return env
|
||||
|
||||
# Walk up from this script to find the cargo target directory.
|
||||
repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
for profile in ("release", "debug"):
|
||||
for ext in ("so", "dylib", "dll"):
|
||||
candidate = repo_root / "target" / profile / f"libquicprochat_ffi.{ext}"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
print(
|
||||
"ERROR: Could not find libquicprochat_ffi. "
|
||||
"Build with: cargo build --release -p quicprochat-ffi\n"
|
||||
"Or set QPQ_FFI_LIB=/path/to/libquicprochat_ffi.so",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _load_library(path: str) -> ctypes.CDLL:
|
||||
"""Load the FFI library and declare function signatures."""
|
||||
lib = ctypes.cdll.LoadLibrary(path)
|
||||
|
||||
# qpq_connect(server, ca_cert, server_name) -> *mut QpqHandle
|
||||
lib.qpq_connect.argtypes = [c_char_p, c_char_p, c_char_p]
|
||||
lib.qpq_connect.restype = c_void_p
|
||||
|
||||
# qpq_login(handle, username, password) -> i32
|
||||
lib.qpq_login.argtypes = [c_void_p, c_char_p, c_char_p]
|
||||
lib.qpq_login.restype = c_int
|
||||
|
||||
# qpq_send(handle, recipient, message, message_len) -> i32
|
||||
lib.qpq_send.argtypes = [c_void_p, c_char_p, ctypes.POINTER(ctypes.c_uint8), c_size_t]
|
||||
lib.qpq_send.restype = c_int
|
||||
|
||||
# qpq_receive(handle, timeout_ms, out_json) -> i32
|
||||
lib.qpq_receive.argtypes = [c_void_p, c_uint32, POINTER(c_char_p)]
|
||||
lib.qpq_receive.restype = c_int
|
||||
|
||||
# qpq_disconnect(handle)
|
||||
lib.qpq_disconnect.argtypes = [c_void_p]
|
||||
lib.qpq_disconnect.restype = None
|
||||
|
||||
# qpq_last_error(handle) -> *const c_char
|
||||
lib.qpq_last_error.argtypes = [c_void_p]
|
||||
lib.qpq_last_error.restype = c_char_p
|
||||
|
||||
# qpq_free_string(ptr)
|
||||
lib.qpq_free_string.argtypes = [c_char_p]
|
||||
lib.qpq_free_string.restype = None
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# High-level wrapper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class QpqClient:
|
||||
"""Synchronous Python client wrapping the quicprochat C FFI."""
|
||||
|
||||
def __init__(self, lib: ctypes.CDLL):
|
||||
self._lib = lib
|
||||
self._handle: int | None = None
|
||||
|
||||
def connect(self, server: str, ca_cert: str, server_name: str) -> None:
|
||||
handle = self._lib.qpq_connect(
|
||||
server.encode(), ca_cert.encode(), server_name.encode(),
|
||||
)
|
||||
if not handle:
|
||||
raise ConnectionError("qpq_connect returned null -- check server address and CA cert")
|
||||
self._handle = handle
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
rc = self._lib.qpq_login(self._handle, username.encode(), password.encode())
|
||||
if rc != QPQ_OK:
|
||||
err = self._last_error()
|
||||
raise RuntimeError(f"qpq_login failed ({_status_name(rc)}): {err}")
|
||||
|
||||
def send(self, recipient: str, message: str) -> None:
|
||||
msg_bytes = message.encode()
|
||||
buf = (ctypes.c_uint8 * len(msg_bytes))(*msg_bytes)
|
||||
rc = self._lib.qpq_send(self._handle, recipient.encode(), buf, len(msg_bytes))
|
||||
if rc != QPQ_OK:
|
||||
err = self._last_error()
|
||||
raise RuntimeError(f"qpq_send failed ({_status_name(rc)}): {err}")
|
||||
|
||||
def receive(self, timeout_ms: int = 5000) -> list[str]:
|
||||
out = c_char_p()
|
||||
rc = self._lib.qpq_receive(self._handle, c_uint32(timeout_ms), ctypes.byref(out))
|
||||
if rc == QPQ_TIMEOUT:
|
||||
return []
|
||||
if rc != QPQ_OK:
|
||||
err = self._last_error()
|
||||
raise RuntimeError(f"qpq_receive failed ({_status_name(rc)}): {err}")
|
||||
|
||||
result: list[str] = []
|
||||
if out.value:
|
||||
result = json.loads(out.value.decode())
|
||||
self._lib.qpq_free_string(out)
|
||||
return result
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self._handle:
|
||||
self._lib.qpq_disconnect(self._handle)
|
||||
self._handle = None
|
||||
|
||||
def _last_error(self) -> str:
|
||||
err = self._lib.qpq_last_error(self._handle)
|
||||
if err:
|
||||
return err.decode(errors="replace")
|
||||
return "(no error message)"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
self.disconnect()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="quicprochat Python FFI client")
|
||||
parser.add_argument("--server", required=True, help="Server address (host:port)")
|
||||
parser.add_argument("--ca-cert", required=True, help="Path to CA certificate (DER)")
|
||||
parser.add_argument("--server-name", default="localhost", help="TLS server name")
|
||||
parser.add_argument("--username", required=True, help="OPAQUE username")
|
||||
parser.add_argument("--password", required=True, help="OPAQUE password")
|
||||
parser.add_argument("--send-to", help="Recipient username for sending a message")
|
||||
parser.add_argument("--message", help="Message text to send")
|
||||
parser.add_argument("--receive", action="store_true", help="Receive pending messages")
|
||||
parser.add_argument("--timeout", type=int, default=5000, help="Receive timeout (ms)")
|
||||
args = parser.parse_args()
|
||||
|
||||
lib_path = _find_library()
|
||||
print(f"Using library: {lib_path}")
|
||||
lib = _load_library(lib_path)
|
||||
|
||||
with QpqClient(lib) as client:
|
||||
print(f"Connecting to {args.server}...")
|
||||
client.connect(args.server, args.ca_cert, args.server_name)
|
||||
print("Connected.")
|
||||
|
||||
print(f"Logging in as {args.username}...")
|
||||
client.login(args.username, args.password)
|
||||
print("Logged in.")
|
||||
|
||||
if args.send_to and args.message:
|
||||
print(f"Sending to {args.send_to}: {args.message}")
|
||||
client.send(args.send_to, args.message)
|
||||
print("Sent.")
|
||||
|
||||
if args.receive:
|
||||
print(f"Receiving (timeout={args.timeout}ms)...")
|
||||
messages = client.receive(timeout_ms=args.timeout)
|
||||
if messages:
|
||||
print(f"Received {len(messages)} message(s):")
|
||||
for i, msg in enumerate(messages, 1):
|
||||
print(f" [{i}] {msg}")
|
||||
else:
|
||||
print("No messages received.")
|
||||
|
||||
print("Disconnecting...")
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user