hivellm-thunder 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # Rulebook - ignore runtime data, keep specs and tasks
2
+ /.rulebook/*
3
+ !/.rulebook/specs/
4
+ !/.rulebook/tasks/
5
+ !/.rulebook/tasks/**/*.md
6
+ !/.rulebook/rulebook.json
7
+
8
+ # Patterns added by @hivellm/rulebook
9
+ # Added at: 2026-07-17T04:49:22.423Z
10
+
11
+ *.log
12
+ *.swo
13
+ *.swp
14
+ *~
15
+ .DS_Store
16
+ .cache/
17
+ .env
18
+ .env.*.local
19
+ .env.local
20
+ .idea/
21
+ .vscode/
22
+ Thumbs.db
23
+ logs/
24
+ temp/
25
+ tmp/
26
+
27
+ CLAUDE.local.md
28
+ .rulebook/backup/
29
+
30
+ # Build artifacts
31
+ rust/target/
32
+ target/
33
+ node_modules/
34
+ dist/
35
+ __pycache__/
36
+ *.pyc
37
+ .venv/
38
+ *.egg-info/
39
+ csharp/**/bin/
40
+ csharp/**/obj/
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivellm-thunder
3
+ Version: 0.1.0
4
+ Summary: HiveLLM binary RPC (Thunder) - wire v1 codec, family profiles, sync/async multiplexed clients
5
+ Project-URL: Homepage, https://github.com/hivellm/thunder
6
+ License-Expression: Apache-2.0
7
+ Keywords: hivellm,messagepack,msgpack,rpc,thunder
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: msgpack>=1.1
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: pyyaml>=6; extra == 'dev'
14
+ Requires-Dist: ruff>=0.6; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # hivellm-thunder
18
+
19
+ **⚡ The HiveLLM binary RPC protocol for Python — wire v1 (frozen), one configurable standard, sync + async multiplexed clients**
20
+
21
+ Thunder is the shared home of the HiveLLM binary RPC standard: a length-prefixed
22
+ MessagePack protocol (`u32 LE length` + body) multiplexing concurrent requests over one
23
+ persistent TCP connection. This package is the Python implementation — import name
24
+ `thunder_rpc`, one runtime dependency (`msgpack`), conformance-tested against the
25
+ language-neutral golden-vector corpus in [`conformance/`](../conformance/).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install hivellm-thunder
31
+ ```
32
+
33
+ ## Quickstart — sync
34
+
35
+ ```python
36
+ from thunder_rpc import Client, ClientConfig, Config, Credentials, Value
37
+
38
+ # Your application's identity on top of the family standard.
39
+ config = Config.standard().with_scheme("myapp").with_port(9000)
40
+
41
+ client_config = ClientConfig(credentials=Credentials.api_key("secret-key"))
42
+ with Client.connect("myapp://localhost", config, client_config) as client:
43
+ pong = client.call("PING")
44
+ assert pong.as_str() == "PONG"
45
+
46
+ hits = client.call("SEARCH", [Value.str("docs"), Value.bytes(embedding)], timeout=5.0)
47
+ ```
48
+
49
+ ## Quickstart — async
50
+
51
+ ```python
52
+ from thunder_rpc import AsyncClient, Config
53
+
54
+ config = Config.standard().with_scheme("myapp").with_port(9000)
55
+
56
+ async with await AsyncClient.connect("myapp://localhost", config) as client:
57
+ pong = await client.call("PING")
58
+ assert pong.as_str() == "PONG"
59
+ ```
60
+
61
+ Both clients implement the identical SPEC-003 contract: pipelined out-of-order
62
+ completion, monotonic u32 ids (skipping `PUSH_ID`), serialized writes, `max_in_flight`
63
+ backpressure, per-call timeouts (default 30 s), lazy 2-attempt reconnect with
64
+ re-handshake, typed errors, and push-frame routing. The asyncio client additionally
65
+ honors task cancellation by removing the pending entry (CLT-021).
66
+
67
+ ## One standard, zero product knowledge
68
+
69
+ Thunder is a protocol library, not a product catalogue: there is no registry of named
70
+ configurations, because a library that must serve implementations which do not exist yet
71
+ cannot ship a hardcoded list of the ones that did. Instead `Config.standard()` is **the**
72
+ family standard (data, not behavior — SPEC-002), pinned to
73
+ [`conformance/standard.yaml`](../conformance/standard.yaml) by the test suite so all four
74
+ language implementations agree on what "standard" means:
75
+
76
+ | Dimension | Standard | Why |
77
+ |---|---|---|
78
+ | `handshake` | `HELLO_MANDATORY` | the only shape that negotiates `proto` and advertises capabilities |
79
+ | `hello_style` | `MAP_PAYLOAD` | `{version, token \| api_key, client_name}`; reply carries proto + capabilities |
80
+ | `push` | `RESERVED` | emitting push is a capability an application opts into |
81
+ | `max_frame_bytes` | 64 MiB | checked before allocation (WIRE-020) |
82
+ | `max_in_flight` | 256 | per-connection request bound |
83
+ | `error_codes` | `BOTH` | `"[code] message"` superset recognizing the RESP3 auth tokens — needs no negotiation |
84
+ | `tls` | `OFF` | additive capability a deployment turns on, never a dialect |
85
+
86
+ `scheme` and `default_port` are deliberately **not** part of the standard: identity is
87
+ your application's, and Thunder has no opinion about it. An application that matches the
88
+ standard writes its identity and nothing else:
89
+
90
+ ```python
91
+ config = Config.standard().with_scheme("myapp").with_port(9000)
92
+ ```
93
+
94
+ An application that still diverges says so in its own repository, where that knowledge
95
+ belongs — every dimension is a `with_*` override returning a new frozen `Config`
96
+ (plain `Config(...)` construction works too):
97
+
98
+ ```python
99
+ config = (
100
+ Config.standard()
101
+ .with_scheme("legacy")
102
+ .with_port(15501)
103
+ .with_handshake(Handshake.AUTH_COMMAND) # AUTH-command auth, no HELLO handler
104
+ .with_hello_style(HelloStyle.NOT_USED)
105
+ .with_push(PushPolicy.ENABLED) # ships a push-producing command
106
+ )
107
+ ```
108
+
109
+ Convergence is then visible and per-application: delete overrides until only identity
110
+ remains. Nobody waits on a Thunder release for a row in a registry (PRO-020).
111
+
112
+ Endpoints accept `scheme://host[:port]` — where the scheme is **your** `config.scheme` —
113
+ or bare `host:port`; `http(s)://` is rejected (Thunder is RPC-only).
114
+
115
+ ## Error classes
116
+
117
+ All errors derive from `thunder_rpc.ThunderError`; branch on the class and `code`, never
118
+ on message text (CLT-052):
119
+
120
+ | Class | Meaning |
121
+ |---|---|
122
+ | `AuthError` | Handshake rejected, or `NOAUTH`/`WRONGPASS`/`NOPERM` reply |
123
+ | `ServerError` | Server answered `Err` — raw `message` + optional bracket `code` |
124
+ | `ConnectionError` | Dial/write failure, dead connection, invalid endpoint |
125
+ | `TimeoutError` | Connect or per-call timeout elapsed |
126
+ | `FrameTooLargeError` | Frame over the config's cap (checked before allocation) |
127
+ | `DecodeError` | Malformed frame, or push frame while push is `RESERVED` |
128
+
129
+ ## Wire layer
130
+
131
+ `thunder_rpc.wire` is pure (no sockets): `encode_frame`, `decode_request`,
132
+ `decode_response` over the 8-variant `Value` model
133
+ (`Null | Bool | Int | Float | Bytes | Str | Array | Map`). Canonical bytes are pinned by
134
+ the corpus: `Bytes` emits as MessagePack bin, floats always pack as f64 bit-exact,
135
+ structs are array-encoded; legacy int-array `Bytes` and map-shaped `Request` decode but
136
+ are never re-emitted.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ pip install -e .[dev]
142
+ python -m pytest # includes the conformance corpus — never skipped
143
+ ruff check .
144
+ ```
145
+
146
+ ## Specs
147
+
148
+ - [SPEC-001 Wire format](../docs/specs/SPEC-001-wire-format.md)
149
+ - [SPEC-002 Protocol configuration](../docs/specs/SPEC-002-configuration.md)
150
+ - [SPEC-003 Client contract](../docs/specs/SPEC-003-client.md)
151
+ - [SPEC-005 Conformance](../docs/specs/SPEC-005-conformance.md)
152
+ - [SPEC-006 Packaging](../docs/specs/SPEC-006-packaging-release.md)
153
+
154
+ Apache-2.0 — part of the [Thunder](../README.md) monorepo release train.
@@ -0,0 +1,138 @@
1
+ # hivellm-thunder
2
+
3
+ **⚡ The HiveLLM binary RPC protocol for Python — wire v1 (frozen), one configurable standard, sync + async multiplexed clients**
4
+
5
+ Thunder is the shared home of the HiveLLM binary RPC standard: a length-prefixed
6
+ MessagePack protocol (`u32 LE length` + body) multiplexing concurrent requests over one
7
+ persistent TCP connection. This package is the Python implementation — import name
8
+ `thunder_rpc`, one runtime dependency (`msgpack`), conformance-tested against the
9
+ language-neutral golden-vector corpus in [`conformance/`](../conformance/).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install hivellm-thunder
15
+ ```
16
+
17
+ ## Quickstart — sync
18
+
19
+ ```python
20
+ from thunder_rpc import Client, ClientConfig, Config, Credentials, Value
21
+
22
+ # Your application's identity on top of the family standard.
23
+ config = Config.standard().with_scheme("myapp").with_port(9000)
24
+
25
+ client_config = ClientConfig(credentials=Credentials.api_key("secret-key"))
26
+ with Client.connect("myapp://localhost", config, client_config) as client:
27
+ pong = client.call("PING")
28
+ assert pong.as_str() == "PONG"
29
+
30
+ hits = client.call("SEARCH", [Value.str("docs"), Value.bytes(embedding)], timeout=5.0)
31
+ ```
32
+
33
+ ## Quickstart — async
34
+
35
+ ```python
36
+ from thunder_rpc import AsyncClient, Config
37
+
38
+ config = Config.standard().with_scheme("myapp").with_port(9000)
39
+
40
+ async with await AsyncClient.connect("myapp://localhost", config) as client:
41
+ pong = await client.call("PING")
42
+ assert pong.as_str() == "PONG"
43
+ ```
44
+
45
+ Both clients implement the identical SPEC-003 contract: pipelined out-of-order
46
+ completion, monotonic u32 ids (skipping `PUSH_ID`), serialized writes, `max_in_flight`
47
+ backpressure, per-call timeouts (default 30 s), lazy 2-attempt reconnect with
48
+ re-handshake, typed errors, and push-frame routing. The asyncio client additionally
49
+ honors task cancellation by removing the pending entry (CLT-021).
50
+
51
+ ## One standard, zero product knowledge
52
+
53
+ Thunder is a protocol library, not a product catalogue: there is no registry of named
54
+ configurations, because a library that must serve implementations which do not exist yet
55
+ cannot ship a hardcoded list of the ones that did. Instead `Config.standard()` is **the**
56
+ family standard (data, not behavior — SPEC-002), pinned to
57
+ [`conformance/standard.yaml`](../conformance/standard.yaml) by the test suite so all four
58
+ language implementations agree on what "standard" means:
59
+
60
+ | Dimension | Standard | Why |
61
+ |---|---|---|
62
+ | `handshake` | `HELLO_MANDATORY` | the only shape that negotiates `proto` and advertises capabilities |
63
+ | `hello_style` | `MAP_PAYLOAD` | `{version, token \| api_key, client_name}`; reply carries proto + capabilities |
64
+ | `push` | `RESERVED` | emitting push is a capability an application opts into |
65
+ | `max_frame_bytes` | 64 MiB | checked before allocation (WIRE-020) |
66
+ | `max_in_flight` | 256 | per-connection request bound |
67
+ | `error_codes` | `BOTH` | `"[code] message"` superset recognizing the RESP3 auth tokens — needs no negotiation |
68
+ | `tls` | `OFF` | additive capability a deployment turns on, never a dialect |
69
+
70
+ `scheme` and `default_port` are deliberately **not** part of the standard: identity is
71
+ your application's, and Thunder has no opinion about it. An application that matches the
72
+ standard writes its identity and nothing else:
73
+
74
+ ```python
75
+ config = Config.standard().with_scheme("myapp").with_port(9000)
76
+ ```
77
+
78
+ An application that still diverges says so in its own repository, where that knowledge
79
+ belongs — every dimension is a `with_*` override returning a new frozen `Config`
80
+ (plain `Config(...)` construction works too):
81
+
82
+ ```python
83
+ config = (
84
+ Config.standard()
85
+ .with_scheme("legacy")
86
+ .with_port(15501)
87
+ .with_handshake(Handshake.AUTH_COMMAND) # AUTH-command auth, no HELLO handler
88
+ .with_hello_style(HelloStyle.NOT_USED)
89
+ .with_push(PushPolicy.ENABLED) # ships a push-producing command
90
+ )
91
+ ```
92
+
93
+ Convergence is then visible and per-application: delete overrides until only identity
94
+ remains. Nobody waits on a Thunder release for a row in a registry (PRO-020).
95
+
96
+ Endpoints accept `scheme://host[:port]` — where the scheme is **your** `config.scheme` —
97
+ or bare `host:port`; `http(s)://` is rejected (Thunder is RPC-only).
98
+
99
+ ## Error classes
100
+
101
+ All errors derive from `thunder_rpc.ThunderError`; branch on the class and `code`, never
102
+ on message text (CLT-052):
103
+
104
+ | Class | Meaning |
105
+ |---|---|
106
+ | `AuthError` | Handshake rejected, or `NOAUTH`/`WRONGPASS`/`NOPERM` reply |
107
+ | `ServerError` | Server answered `Err` — raw `message` + optional bracket `code` |
108
+ | `ConnectionError` | Dial/write failure, dead connection, invalid endpoint |
109
+ | `TimeoutError` | Connect or per-call timeout elapsed |
110
+ | `FrameTooLargeError` | Frame over the config's cap (checked before allocation) |
111
+ | `DecodeError` | Malformed frame, or push frame while push is `RESERVED` |
112
+
113
+ ## Wire layer
114
+
115
+ `thunder_rpc.wire` is pure (no sockets): `encode_frame`, `decode_request`,
116
+ `decode_response` over the 8-variant `Value` model
117
+ (`Null | Bool | Int | Float | Bytes | Str | Array | Map`). Canonical bytes are pinned by
118
+ the corpus: `Bytes` emits as MessagePack bin, floats always pack as f64 bit-exact,
119
+ structs are array-encoded; legacy int-array `Bytes` and map-shaped `Request` decode but
120
+ are never re-emitted.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ pip install -e .[dev]
126
+ python -m pytest # includes the conformance corpus — never skipped
127
+ ruff check .
128
+ ```
129
+
130
+ ## Specs
131
+
132
+ - [SPEC-001 Wire format](../docs/specs/SPEC-001-wire-format.md)
133
+ - [SPEC-002 Protocol configuration](../docs/specs/SPEC-002-configuration.md)
134
+ - [SPEC-003 Client contract](../docs/specs/SPEC-003-client.md)
135
+ - [SPEC-005 Conformance](../docs/specs/SPEC-005-conformance.md)
136
+ - [SPEC-006 Packaging](../docs/specs/SPEC-006-packaging-release.md)
137
+
138
+ Apache-2.0 — part of the [Thunder](../README.md) monorepo release train.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hivellm-thunder"
7
+ version = "0.1.0"
8
+ description = "HiveLLM binary RPC (Thunder) - wire v1 codec, family profiles, sync/async multiplexed clients"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["msgpack>=1.1"]
13
+ keywords = ["rpc", "messagepack", "msgpack", "hivellm", "thunder"]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8",
18
+ "pytest-asyncio>=0.24",
19
+ "PyYAML>=6",
20
+ "ruff>=0.6",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/hivellm/thunder"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["thunder_rpc"]
28
+
29
+ [tool.ruff]
30
+ line-length = 100
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ asyncio_mode = "auto"
35
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,4 @@
1
+ msgpack>=1.1
2
+ PyYAML>=6
3
+ pytest>=8
4
+ pytest-asyncio>=0.24
@@ -0,0 +1,12 @@
1
+ """Test-suite plumbing: guarantees the tests directory is importable
2
+ (``mockserver``) and the package resolves from a source checkout."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ _HERE = Path(__file__).resolve().parent
10
+ for path in (str(_HERE), str(_HERE.parent)):
11
+ if path not in sys.path:
12
+ sys.path.insert(0, path)
@@ -0,0 +1,121 @@
1
+ """Scripted loopback TCP responders for the behavioral floor tests
2
+ (SPEC-003 / CLT-090) — the Python mirror of the tokio responders in
3
+ ``rust/thunder-client/tests/behavior.rs``. Built on the thunder_rpc wire
4
+ codec; serves both the sync and the asyncio client (the server side is
5
+ always a plain thread)."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import socket
10
+ import threading
11
+ from typing import Callable
12
+
13
+ from thunder_rpc import PUSH_ID, Request, Response, Value, wire
14
+
15
+ #: Frame cap the loopback responders read with.
16
+ SRV_CAP = 1024 * 1024
17
+
18
+ #: Generous safety timeout so a broken test fails instead of hanging.
19
+ IO_TIMEOUT = 10.0
20
+
21
+
22
+ class PeerClosed(Exception):
23
+ """The client closed the connection while the script was reading."""
24
+
25
+
26
+ class ServerConn:
27
+ """One accepted connection, with frame-level helpers."""
28
+
29
+ def __init__(self, sock: socket.socket) -> None:
30
+ self.sock = sock
31
+ sock.settimeout(IO_TIMEOUT)
32
+
33
+ def read_request(self) -> Request:
34
+ header = self._read_exact(4)
35
+ length = int.from_bytes(header, "little")
36
+ assert length <= SRV_CAP, f"client sent an over-cap frame ({length} bytes)"
37
+ body = self._read_exact(length)
38
+ return wire.decode_request_body(body)
39
+
40
+ def send_ok(self, frame_id: int, value: Value) -> None:
41
+ self.send_raw(wire.encode_frame(Response(id=frame_id, ok=value)))
42
+
43
+ def send_err(self, frame_id: int, message: str) -> None:
44
+ self.send_raw(wire.encode_frame(Response(id=frame_id, err=message)))
45
+
46
+ def send_push(self, value: Value) -> None:
47
+ self.send_raw(wire.encode_frame(Response(id=PUSH_ID, ok=value)))
48
+
49
+ def send_raw(self, data: bytes) -> None:
50
+ self.sock.sendall(data)
51
+
52
+ def close(self) -> None:
53
+ try:
54
+ self.sock.shutdown(socket.SHUT_RDWR)
55
+ except OSError:
56
+ pass
57
+ try:
58
+ self.sock.close()
59
+ except OSError:
60
+ pass
61
+
62
+ def _read_exact(self, size: int) -> bytes:
63
+ buf = bytearray(size)
64
+ view = memoryview(buf)
65
+ got = 0
66
+ while got < size:
67
+ read = self.sock.recv_into(view[got:], size - got)
68
+ if read == 0:
69
+ raise PeerClosed()
70
+ got += read
71
+ return bytes(buf)
72
+
73
+
74
+ class MockServer:
75
+ """Runs ``script(server)`` on a background thread; the script drives
76
+ ``accept()`` / frame helpers. Use as a context manager — exit joins the
77
+ script and re-raises anything it tripped on."""
78
+
79
+ def __init__(self, script: Callable[["MockServer"], None]) -> None:
80
+ self._script = script
81
+ self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82
+ self._listener.bind(("127.0.0.1", 0))
83
+ self._listener.listen(8)
84
+ self._listener.settimeout(IO_TIMEOUT)
85
+ self.port = self._listener.getsockname()[1]
86
+ #: Endpoint string the clients dial (bare host:port, CLT-070).
87
+ self.address = f"127.0.0.1:{self.port}"
88
+ #: How many connections the script accepted (reconnect assertions).
89
+ self.accepts = 0
90
+ self._conns: list[ServerConn] = []
91
+ self._error: BaseException | None = None
92
+ self._thread = threading.Thread(
93
+ target=self._run, name="mock-server", daemon=True
94
+ )
95
+
96
+ def accept(self) -> ServerConn:
97
+ sock, _ = self._listener.accept()
98
+ self.accepts += 1
99
+ conn = ServerConn(sock)
100
+ self._conns.append(conn)
101
+ return conn
102
+
103
+ def _run(self) -> None:
104
+ try:
105
+ self._script(self)
106
+ except BaseException as exc: # surface script failures on __exit__
107
+ self._error = exc
108
+
109
+ def __enter__(self) -> "MockServer":
110
+ self._thread.start()
111
+ return self
112
+
113
+ def __exit__(self, exc_type: object, *_exc: object) -> None:
114
+ self._thread.join(timeout=IO_TIMEOUT + 5)
115
+ for conn in self._conns:
116
+ conn.close()
117
+ self._listener.close()
118
+ if exc_type is None:
119
+ assert not self._thread.is_alive(), "mock server script did not finish"
120
+ if self._error is not None:
121
+ raise self._error