bunqueue-client 0.1.5__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.
- bunqueue_client-0.1.5/.gitignore +5 -0
- bunqueue_client-0.1.5/CHANGELOG.md +183 -0
- bunqueue_client-0.1.5/CLAUDE.md +89 -0
- bunqueue_client-0.1.5/LICENSE +21 -0
- bunqueue_client-0.1.5/PKG-INFO +197 -0
- bunqueue_client-0.1.5/README.md +171 -0
- bunqueue_client-0.1.5/bunqueue/__init__.py +69 -0
- bunqueue_client-0.1.5/bunqueue/ack_batcher.py +93 -0
- bunqueue_client-0.1.5/bunqueue/connection.py +240 -0
- bunqueue_client-0.1.5/bunqueue/connection_lifecycle.py +104 -0
- bunqueue_client-0.1.5/bunqueue/errors.py +38 -0
- bunqueue_client-0.1.5/bunqueue/events.py +56 -0
- bunqueue_client-0.1.5/bunqueue/flow.py +257 -0
- bunqueue_client-0.1.5/bunqueue/job.py +179 -0
- bunqueue_client-0.1.5/bunqueue/options.py +126 -0
- bunqueue_client-0.1.5/bunqueue/py.typed +0 -0
- bunqueue_client-0.1.5/bunqueue/queue.py +230 -0
- bunqueue_client-0.1.5/bunqueue/queue_admin.py +201 -0
- bunqueue_client-0.1.5/bunqueue/queue_query.py +204 -0
- bunqueue_client-0.1.5/bunqueue/simple/__init__.py +16 -0
- bunqueue_client-0.1.5/bunqueue/simple/aging.py +68 -0
- bunqueue_client-0.1.5/bunqueue/simple/app.py +163 -0
- bunqueue_client-0.1.5/bunqueue/simple/app_api.py +142 -0
- bunqueue_client-0.1.5/bunqueue/simple/batch.py +88 -0
- bunqueue_client-0.1.5/bunqueue/simple/cancellation.py +79 -0
- bunqueue_client-0.1.5/bunqueue/simple/circuit_breaker.py +93 -0
- bunqueue_client-0.1.5/bunqueue/simple/dedup_debounce.py +42 -0
- bunqueue_client-0.1.5/bunqueue/simple/rate_gate.py +57 -0
- bunqueue_client-0.1.5/bunqueue/simple/retry.py +65 -0
- bunqueue_client-0.1.5/bunqueue/simple/triggers.py +45 -0
- bunqueue_client-0.1.5/bunqueue/simple/ttl.py +34 -0
- bunqueue_client-0.1.5/bunqueue/telemetry.py +29 -0
- bunqueue_client-0.1.5/bunqueue/transport.py +50 -0
- bunqueue_client-0.1.5/bunqueue/wire.py +99 -0
- bunqueue_client-0.1.5/bunqueue/worker.py +259 -0
- bunqueue_client-0.1.5/bunqueue/worker_runtime.py +221 -0
- bunqueue_client-0.1.5/pyproject.toml +41 -0
- bunqueue_client-0.1.5/tests/e2e_admin.py +154 -0
- bunqueue_client-0.1.5/tests/e2e_audit_fixes.py +529 -0
- bunqueue_client-0.1.5/tests/e2e_auth.py +56 -0
- bunqueue_client-0.1.5/tests/e2e_control.py +133 -0
- bunqueue_client-0.1.5/tests/e2e_edge.py +223 -0
- bunqueue_client-0.1.5/tests/e2e_flow.py +149 -0
- bunqueue_client-0.1.5/tests/e2e_hardening.py +125 -0
- bunqueue_client-0.1.5/tests/e2e_query.py +110 -0
- bunqueue_client-0.1.5/tests/e2e_realistic.py +182 -0
- bunqueue_client-0.1.5/tests/e2e_serialization.py +107 -0
- bunqueue_client-0.1.5/tests/e2e_simple.py +213 -0
- bunqueue_client-0.1.5/tests/e2e_simple_extras.py +179 -0
- bunqueue_client-0.1.5/tests/e2e_spec_align.py +147 -0
- bunqueue_client-0.1.5/tests/e2e_telemetry.py +50 -0
- bunqueue_client-0.1.5/tests/e2e_worker.py +263 -0
- bunqueue_client-0.1.5/tests/harness.py +108 -0
- bunqueue_client-0.1.5/tests/run_audit.py +27 -0
- bunqueue_client-0.1.5/tests/run_e2e.py +48 -0
- bunqueue_client-0.1.5/tests/soak.py +63 -0
- bunqueue_client-0.1.5/tests/test_integration.py +206 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `bunqueue-client` (Python SDK) are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.5] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
First version published on PyPI: `pip install bunqueue-client`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Reject MessagePack payloads larger than the protocol's 64 MiB frame cap
|
|
15
|
+
locally with `SerializationError`, before registering a pending request or
|
|
16
|
+
writing to the socket.
|
|
17
|
+
- Recursively validate commands before writing: cyclic containers, non-string
|
|
18
|
+
map keys, and integers beyond float64 range now raise `SerializationError`
|
|
19
|
+
without leaking pending requests. Shared containers remain valid; binary
|
|
20
|
+
values and list/tuple arrays keep their wire representation.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Dependency-free structured transport telemetry through the optional
|
|
25
|
+
`on_telemetry` callback, with lifecycle, auth, command latency, timeout,
|
|
26
|
+
reconnect, and error events. Consumer exceptions are isolated.
|
|
27
|
+
- Focused real-server telemetry tests. Connection lifecycle and frame helpers
|
|
28
|
+
were split into single-responsibility modules below the 300-line source cap.
|
|
29
|
+
- Add independent-thread idempotency and single-lease races, fixed-seed
|
|
30
|
+
generated payload invariants, malformed mutation fuzzing, and an opt-in
|
|
31
|
+
sustained producer profile.
|
|
32
|
+
|
|
33
|
+
## [0.1.4] - 2026-07-14
|
|
34
|
+
|
|
35
|
+
Conformance-suite driven: the SDK is now certified by the cross-language
|
|
36
|
+
conformance kit (`sdk/conformance`, 17/17) against the formal wire spec
|
|
37
|
+
(`docs/protocol.md`).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`drain()` now returns the number of removed jobs** (was `None`,
|
|
42
|
+
silently discarding the wire `count`).
|
|
43
|
+
|
|
44
|
+
## [0.1.3] - 2026-07-14
|
|
45
|
+
|
|
46
|
+
Spec-alignment audit against the core protocol. Every fix ships with a repro
|
|
47
|
+
test in `tests/e2e_spec_align.py`.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`heartbeat_interval_s=0` now disables heartbeats.** Previously
|
|
52
|
+
`Event.wait(0)` made the heartbeat loop busy-spin, flooding the server with
|
|
53
|
+
`Heartbeat` commands. `0` (or negative) now matches the official client's
|
|
54
|
+
"0 = disabled" semantics.
|
|
55
|
+
- **`batch_size` is clamped to the server maximum (1000).** The server
|
|
56
|
+
rejects `PULLB` with `count > 1000`; an unclamped `batch_size` combined
|
|
57
|
+
with `concurrency > 1000` wedged the poll loop in a permanent error cycle.
|
|
58
|
+
- **FAIL stack truncation no longer loses the raise site.** The worker sent
|
|
59
|
+
the last 20 traceback lines but the server persists only the FIRST
|
|
60
|
+
`stackTraceLimit` lines (default 10), so long tracebacks kept a middle
|
|
61
|
+
window without the raise site. The worker now sends at most as many
|
|
62
|
+
trailing lines as the server keeps, honoring a per-job `stackTraceLimit`.
|
|
63
|
+
- **Simple Mode `cron()`/`every()` forward the execution `limit=`.** The
|
|
64
|
+
option was silently dropped (the "client drops a wire-supported field"
|
|
65
|
+
class, #111); it now reaches the scheduler as wire `maxLimit`.
|
|
66
|
+
- **`wait_for_job()` clamps `timeout_ms` to the server cap (600000).**
|
|
67
|
+
Larger values were rejected by the server with "timeout must be at most
|
|
68
|
+
600000" instead of waiting.
|
|
69
|
+
- **`PROTOCOL_VERSION` bumped to 2**, matching the version the server
|
|
70
|
+
advertises in `Hello`.
|
|
71
|
+
|
|
72
|
+
## [0.1.2] - 2026-07-10
|
|
73
|
+
|
|
74
|
+
Second audit pass: packaging, connection failure paths, worker lifecycle
|
|
75
|
+
edges. New fixes ship with repro tests in `tests/e2e_audit_fixes.py` and
|
|
76
|
+
`tests/e2e_worker.py`.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Opt-in ACKB batching for the Worker** (TS SDK parity):
|
|
81
|
+
`Worker(..., ack_batch={"max_size": 50, "max_delay_ms": 5})` buffers
|
|
82
|
+
successful ACKs and flushes them as a single `ACKB` round-trip on size,
|
|
83
|
+
delay, or close. A job stays active (lock renewed) until its batch settles;
|
|
84
|
+
on a failed batch each job gets an `error` event and `completed` is NOT
|
|
85
|
+
emitted. (H2)
|
|
86
|
+
- **PEP 561**: the package now ships `bunqueue/py.typed`, so type checkers
|
|
87
|
+
consume the inline hints; `Typing :: Typed` classifier added. (H1)
|
|
88
|
+
- **Logging**: a `logging.getLogger("bunqueue")` logger (NullHandler attached
|
|
89
|
+
in `__init__`) now surfaces the previously silent failure points at warning
|
|
90
|
+
level: `_safe_call` swallowed errors, raising event listeners, failed worker
|
|
91
|
+
registrations, priority-aging ticks, ACKB settle-callback errors. (H2)
|
|
92
|
+
- `Worker` is now a context manager (`with Worker(...) as w:`), matching
|
|
93
|
+
`Queue` and `FlowProducer`. (M3)
|
|
94
|
+
- `SerializationError` (subclass of `BunqueueError`), raised when a command
|
|
95
|
+
payload cannot be msgpack-serialized; the original error is chained. (M1)
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- **Pending-future leak on serialization failure.** `Connection._send`
|
|
100
|
+
registered the reqId future before `msgpack.packb`; an unserializable
|
|
101
|
+
payload (e.g. a `datetime` in job data) leaked the entry forever and
|
|
102
|
+
surfaced as a raw `TypeError`. Payloads now serialize first and failures
|
|
103
|
+
raise `SerializationError`. (M1)
|
|
104
|
+
- **TLS handshake failures** now close the raw socket (no fd leak), count into
|
|
105
|
+
the same reconnect backoff as plain connect failures, and raise
|
|
106
|
+
`ConnectionClosedError` with the `ssl` error chained, instead of leaking a
|
|
107
|
+
raw `ssl.SSLError`. (M2)
|
|
108
|
+
- **Worker close edges**: `close()` with `autorun=False` and `run()` never
|
|
109
|
+
called now marks the worker closed and closes its connection; an expired
|
|
110
|
+
`close(timeout)` returns `False` and keeps the live thread reference (state
|
|
111
|
+
stays honest, a later `close()` joins again) instead of nulling it. (M3)
|
|
112
|
+
- **Register false-success.** A failed `RegisterWorker` no longer marks the
|
|
113
|
+
generation as registered, so the next poll iteration retries; previously the
|
|
114
|
+
server could stay unaware of the worker until the next reconnect
|
|
115
|
+
(Discussion #103 class). (M5)
|
|
116
|
+
- **Scheduler template priority/deduplication** (#111 class, TS SDK parity):
|
|
117
|
+
`upsert_job_scheduler` now sends the template's `priority` and
|
|
118
|
+
`deduplication` (`uniqueKey`/`dedup`) as top-level `Cron` fields, where the
|
|
119
|
+
server actually reads them; inside `jobOptions` they were silently ignored
|
|
120
|
+
and spawned jobs fell back to defaults.
|
|
121
|
+
- **move_job_to_failed with an exception** (#111 class, TS SDK parity): when
|
|
122
|
+
passed an `Exception`, the FAIL command now carries `stack` (bounded
|
|
123
|
+
traceback lines, last-lines like the worker path) and the `unrecoverable`
|
|
124
|
+
flag for `UnrecoverableError`, so the failure intent and stacktrace persist
|
|
125
|
+
server-side. String errors travel unchanged.
|
|
126
|
+
|
|
127
|
+
### Verified
|
|
128
|
+
|
|
129
|
+
- Not-found narrowing: `get_job`, `get_job_by_custom_id`, `get_job_scheduler`
|
|
130
|
+
and `get_flow` already catch only `CommandError` with a "not found" message
|
|
131
|
+
(mapping it to `None`) and rethrow everything else; connection/timeout
|
|
132
|
+
failures never masquerade as a missing job. Regression test added.
|
|
133
|
+
|
|
134
|
+
### Packaging
|
|
135
|
+
|
|
136
|
+
- `LICENSE` (MIT) now ships with the sdist/wheel; pyproject uses the SPDX
|
|
137
|
+
`license = "MIT"` expression with `license-files` (hatchling >= 1.27).
|
|
138
|
+
- Classifiers for Python 3.9 through 3.13; `Repository` and `Changelog`
|
|
139
|
+
project URLs. (M4)
|
|
140
|
+
|
|
141
|
+
## [0.1.1] - 2026-07-08
|
|
142
|
+
|
|
143
|
+
Protocol-coherence audit against the bunqueue server. Every fix ships with a
|
|
144
|
+
RED→GREEN repro in `tests/e2e_audit_fixes.py`.
|
|
145
|
+
|
|
146
|
+
### Fixed
|
|
147
|
+
|
|
148
|
+
- **add_bulk dropped the custom job id.** PUSHB entries are `JobInput`
|
|
149
|
+
(`customId`), not the single-PUSH `jobId` the server renames — the batch
|
|
150
|
+
path now renames `jobId`→`customId`, so `get_job_by_custom_id` and idempotent
|
|
151
|
+
bulk ingest work. (H1)
|
|
152
|
+
- **Half-open link wedge.** Enable `SO_KEEPALIVE` (~15s idle) and tear down the
|
|
153
|
+
socket after 3 consecutive command timeouts so the next call reconnects,
|
|
154
|
+
instead of wedging until the OS abandons the writes. The teardown is
|
|
155
|
+
generation-guarded so a stale-connection timeout can't abort a fresh
|
|
156
|
+
reconnect. (H2)
|
|
157
|
+
- **Auth race on reconnect.** `_conn_lock` is now reentrant and Auth is sent
|
|
158
|
+
while holding it, flipping `_connected` only after Auth completes — a
|
|
159
|
+
concurrent thread can no longer send a command ahead of the Auth frame
|
|
160
|
+
(server would reject it `Not authenticated`). (H3)
|
|
161
|
+
- **get_flow crashed on a missing job.** A missing root/child now yields `None`
|
|
162
|
+
and is skipped (partial tree) instead of raising; the catch is narrowed to
|
|
163
|
+
`'not found'` so real server errors still surface, and a `visited` set guards
|
|
164
|
+
against cycles now that `depth` defaults to unlimited. (H4)
|
|
165
|
+
- **wait_for_job returned `None` on timeout.** It now raises on non-completion:
|
|
166
|
+
a `failed` job raises `CommandError`, otherwise `CommandTimeoutError` — the
|
|
167
|
+
`completed` flag is no longer ignored. (M1)
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
|
|
171
|
+
- Simple Mode cron (`Bunqueue.cron`/`every`, `upsert_job_scheduler`) now maps
|
|
172
|
+
Pythonic job options snake_case→camelCase via `build_cron_job_options`
|
|
173
|
+
(`attempts`→`maxAttempts`, `remove_on_complete`→`removeOnComplete`, …) so
|
|
174
|
+
cron-spawned jobs honor the requested retry/cleanup policy instead of falling
|
|
175
|
+
back to server defaults; also forwards `skip_missed_on_restart`. (M3)
|
|
176
|
+
- `retry_dlq` / `retry_jobs`: the dead `count` field is no longer sent on the
|
|
177
|
+
wire (the server has no partial RetryDlq; `count` is accepted only for
|
|
178
|
+
signature parity).
|
|
179
|
+
|
|
180
|
+
## [0.1.0] - initial published release
|
|
181
|
+
|
|
182
|
+
- TCP client (msgpack wire protocol): `Queue`, `Worker`, `FlowProducer`,
|
|
183
|
+
`Bunqueue` Simple Mode, TLS, auth, reqId pipelining.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# bunqueue Python SDK — development guide
|
|
2
|
+
|
|
3
|
+
Python client SDK for the bunqueue server. Speaks ONLY the native TCP
|
|
4
|
+
protocol (msgpack). Parity reference: the official TypeScript client in
|
|
5
|
+
`../../src/client/` (TCP mode — embedded/sandboxed/QueueEvents are excluded
|
|
6
|
+
by design: they require the in-process Bun runtime).
|
|
7
|
+
|
|
8
|
+
## Non-negotiable rules
|
|
9
|
+
|
|
10
|
+
1. **Never touch the bunqueue core** (`../../src/`): this SDK lives here only.
|
|
11
|
+
2. **Max 250 lines per file** — split into modules/mixins, don't compress.
|
|
12
|
+
3. **Single runtime dependency: `msgpack`** — nothing else.
|
|
13
|
+
4. **Protocol source of truth**: `../../src/domain/types/command.ts`
|
|
14
|
+
(commands), `../../src/domain/types/response.ts` (responses),
|
|
15
|
+
`../../src/infrastructure/server/handlers/` (actual shapes). When in
|
|
16
|
+
doubt, read the handler — never guess.
|
|
17
|
+
5. Every new method → matching e2e test in `tests/e2e_*.py`.
|
|
18
|
+
6. All docs, comments, and identifiers in English.
|
|
19
|
+
|
|
20
|
+
## Module map
|
|
21
|
+
|
|
22
|
+
| File | Role |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `wire.py` | Wire helpers: `_compact` (drop None), `_js_safe` (int→float64), TLS context |
|
|
25
|
+
| `connection.py` | Socket + reader thread, reqId→Future pipelining, auth, lazy reconnect |
|
|
26
|
+
| `errors.py` | Hierarchy: `BunqueueError` → Connection/Timeout/Command/Auth + `UnrecoverableError` |
|
|
27
|
+
| `events.py` | Thread-safe EventEmitter (on/once/off/emit) |
|
|
28
|
+
| `options.py` | Pythonic kwargs → PUSH wire fields (`attempts`→`maxAttempts`, etc.) |
|
|
29
|
+
| `job.py` | Job wrapper: properties + per-id operations (progress, log, lock, retry…) |
|
|
30
|
+
| `queue.py` | Queue core: produce + control; composes the mixins |
|
|
31
|
+
| `queue_query.py` | Query mixin: getJob(s), states, counts, logs, children values |
|
|
32
|
+
| `queue_admin.py` | Admin mixin: DLQ, configs, rate limit, schedulers/cron, webhooks, monitoring |
|
|
33
|
+
| `worker.py` | Worker lifecycle: start/run/pause/close, events |
|
|
34
|
+
| `worker_runtime.py` | Worker runtime mixin: poll loop, job execution, heartbeats, registry |
|
|
35
|
+
| `flow.py` | FlowProducer: tree/chain/fan-in, UpdateParent, rollback |
|
|
36
|
+
| `simple/app.py` + `simple/app_api.py` | `Bunqueue` Simple Mode: constructor + processing pipeline, API mixin |
|
|
37
|
+
| `simple/{retry,circuit_breaker,batch,triggers,aging,cancellation,ttl,dedup_debounce}.py` | Simple Mode subsystems, 1:1 with `src/client/bunqueue/` |
|
|
38
|
+
|
|
39
|
+
## Wire protocol (VITAL gotchas)
|
|
40
|
+
|
|
41
|
+
- Frame = 4-byte big-endian u32 length + standard msgpack map. Request
|
|
42
|
+
`{cmd, reqId, ...}`; response `{ok, reqId, ...}`; `ok:false` → `error` field.
|
|
43
|
+
- Auth = first command `{cmd:'Auth', token}`. `Hello` for version negotiation.
|
|
44
|
+
- **BigInt killer**: Python ints outside the int32 range travel as
|
|
45
|
+
int64/uint64 msgpack → the server (msgpackr) decodes them as `BigInt` →
|
|
46
|
+
arithmetic crash (e.g. `ListWorkers`). `_js_safe()` converts them to
|
|
47
|
+
float64 (exact ≤ 2^53). NEVER remove this conversion.
|
|
48
|
+
- The **job name travels inside `data`**: `data = {"name": ..., **user}`.
|
|
49
|
+
- Responses **wrapped in `data`**: `GetLogs→data.logs`,
|
|
50
|
+
`ListWorkers→data.workers`, `GetChildrenValues→data.values`,
|
|
51
|
+
`AddWebhook→data.webhookId`, `ListWebhooks→data.webhooks`, `Ping→data.pong`.
|
|
52
|
+
- Responses **top-level**: `IsPaused→paused`, `CronGet→cron`,
|
|
53
|
+
`GetProgress→progress/message`, `PULL→job+token`, `PULLB→jobs+tokens`,
|
|
54
|
+
`PUSH→id`, `PUSHB→ids`, `Count/Clean→count`.
|
|
55
|
+
- `Progress` only works on **active** jobs (errors on waiting).
|
|
56
|
+
- `lifo` orders only among jobs that are **both** lifo; mixed → FIFO by runAt.
|
|
57
|
+
- `GetJobs`/`PromoteJobs` read from SQLite: the write buffer flushes every
|
|
58
|
+
~10ms → in tests wait with `wait_until`, never assert right after a push.
|
|
59
|
+
- Not-found (`GetJob`, `GetJobByCustomId`, `CronGet`) → the server answers a
|
|
60
|
+
"not found" error: the SDK maps it to `None`; don't leak the exception.
|
|
61
|
+
- Webhooks: localhost/private URLs rejected (SSRF guard); valid events look
|
|
62
|
+
like `job.completed`.
|
|
63
|
+
- `retry_job` over TCP = `MoveToWait`; `retry_jobs("failed")` = `RetryDlq`;
|
|
64
|
+
`retry_jobs("completed")` = `RetryCompleted`.
|
|
65
|
+
- `FAIL` supports `unrecoverable: true` (skip retries → DLQ) and
|
|
66
|
+
`stack: string[]` (persisted, capped at `stackTraceLimit`).
|
|
67
|
+
|
|
68
|
+
## Tests
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3 -m venv .venv && .venv/bin/pip install msgpack # once
|
|
72
|
+
.venv/bin/python tests/test_integration.py # smoke (8) — also pytest-compatible
|
|
73
|
+
.venv/bin/python tests/run_e2e.py # full e2e (112)
|
|
74
|
+
BUNQUEUE_SDK_SOAK_SECONDS=3600 .venv/bin/python tests/soak.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Both spawn a real server (`bun src/main.ts` from the repo root, random port,
|
|
78
|
+
temp DB). `tests/harness.py` = server fixture + `@test` registry +
|
|
79
|
+
`wait_until`. The auth suite uses a dedicated server with `AUTH_TOKENS`.
|
|
80
|
+
|
|
81
|
+
## New-method checklist
|
|
82
|
+
|
|
83
|
+
1. Find the command in `command.ts` and the response shape in its handler.
|
|
84
|
+
2. Add the method in the right mixin (query/admin/core) — snake_case mirror
|
|
85
|
+
of the TS name (`getJobCounts` → `get_job_counts`).
|
|
86
|
+
3. `_compact()` the payload; unwrap the response correctly (see gotchas).
|
|
87
|
+
4. Add an e2e test in the matching `tests/e2e_<area>.py`.
|
|
88
|
+
5. `run_e2e.py` + `test_integration.py` green. Update the README if the
|
|
89
|
+
public surface changed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Egeo Minotti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bunqueue-client
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Python client SDK for bunqueue — high-performance job queue server (TCP mode)
|
|
5
|
+
Project-URL: Homepage, https://github.com/egeominotti/bunqueue
|
|
6
|
+
Project-URL: Documentation, https://bunqueue.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/egeominotti/bunqueue
|
|
8
|
+
Project-URL: Changelog, https://github.com/egeominotti/bunqueue/blob/main/sdk/python/CHANGELOG.md
|
|
9
|
+
Author: Egeo Minotti
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: bunqueue,jobs,queue,task-queue,worker
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: msgpack>=1.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<div align="center">
|
|
28
|
+
|
|
29
|
+
<a href="https://bunqueue.dev">
|
|
30
|
+
<img src="https://raw.githubusercontent.com/egeominotti/bunqueue/main/.github/logo.png" alt="bunqueue logo" width="110" />
|
|
31
|
+
</a>
|
|
32
|
+
|
|
33
|
+
# bunqueue-client (Python)
|
|
34
|
+
|
|
35
|
+
**The official Python client for [bunqueue](https://bunqueue.dev), the high performance job queue server.**
|
|
36
|
+
|
|
37
|
+
Native TCP protocol (msgpack, pipelined), one runtime dependency (`msgpack`), sync plus thread based workers.
|
|
38
|
+
|
|
39
|
+
[](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE)
|
|
40
|
+
[](https://github.com/egeominotti/bunqueue/tree/main/sdk/python)
|
|
41
|
+
[](https://github.com/egeominotti/bunqueue/tree/main/sdk/conformance)
|
|
42
|
+
|
|
43
|
+
[Documentation](https://bunqueue.dev/guide/sdks/) · [Protocol spec](https://github.com/egeominotti/bunqueue/blob/main/docs/protocol.md) · [Server](https://github.com/egeominotti/bunqueue) · [Changelog](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/CHANGELOG.md)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Python client for [bunqueue](https://github.com/egeominotti/bunqueue), the
|
|
50
|
+
high-performance job queue server for Bun. Talks the native TCP protocol
|
|
51
|
+
(msgpack, pipelined) with the same core API as the TypeScript SDK, including
|
|
52
|
+
opt-in ACK batching and dependency-free structured transport telemetry.
|
|
53
|
+
Connection pooling remains TypeScript-only for now.
|
|
54
|
+
|
|
55
|
+
The bunqueue **server** runs on Bun (or as a compiled binary / Docker). This
|
|
56
|
+
SDK lets any Python service produce and consume jobs on it: *one queue, any
|
|
57
|
+
language*.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# PyPI release coming soon; install from the repo today:
|
|
63
|
+
pip install "git+https://github.com/egeominotti/bunqueue.git#subdirectory=sdk/python"
|
|
64
|
+
# dependency: msgpack; import name: bunqueue
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python ≥ 3.9 and a running bunqueue server (`bunx bunqueue start`).
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from bunqueue import Queue, Worker
|
|
73
|
+
|
|
74
|
+
# Producer
|
|
75
|
+
queue = Queue("emails", host="localhost", port=6789)
|
|
76
|
+
job = queue.add("send", {"to": "user@example.com"}, priority=5, attempts=3)
|
|
77
|
+
print(job.id)
|
|
78
|
+
|
|
79
|
+
# Consumer
|
|
80
|
+
def process(job):
|
|
81
|
+
job.update_progress(50)
|
|
82
|
+
return {"sent": True}
|
|
83
|
+
|
|
84
|
+
worker = Worker("emails", process, concurrency=10)
|
|
85
|
+
worker.on("completed", lambda job, result: print(job.id, result))
|
|
86
|
+
worker.run() # blocking; or worker.start() for a background thread
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
All queue semantics (retry, backoff, DLQ, priorities, stall detection, cron)
|
|
90
|
+
run **server-side** — the worker only pulls, heartbeats, and acks.
|
|
91
|
+
|
|
92
|
+
## Feature surface
|
|
93
|
+
|
|
94
|
+
- **Queue**: `add`, `add_bulk`, full job options (priority, delay, attempts,
|
|
95
|
+
backoff, ttl, timeout, job_id, deduplication, depends_on, tags, group_id,
|
|
96
|
+
lifo, remove_on_complete/fail, durable, repeat, debounce, …)
|
|
97
|
+
- **Query**: `get_job`, `get_job_by_custom_id`, `get_jobs` (+ per-state
|
|
98
|
+
helpers), `get_state`, `get_result`, `get_progress`, `wait_for_job`
|
|
99
|
+
(raises `CommandTimeoutError` on timeout, BullMQ contract),
|
|
100
|
+
counts (+ per-priority), children values, job logs
|
|
101
|
+
- **Control**: pause/resume/drain/obliterate/clean, remove, discard,
|
|
102
|
+
promote (single/bulk), retry_job / retry_jobs, move to wait/delayed,
|
|
103
|
+
change priority/delay, update data, extend lock
|
|
104
|
+
- **DLQ**: `get_dlq`, `retry_dlq`, `purge_dlq`, DLQ config
|
|
105
|
+
- **Schedulers**: `upsert_job_scheduler` (cron pattern or interval),
|
|
106
|
+
`add_cron` / `every` shorthands, get/list/remove
|
|
107
|
+
- **Admin**: rate limit, global concurrency, stall config, webhooks,
|
|
108
|
+
stats/metrics/list_queues/get_workers
|
|
109
|
+
- **Worker**: events (`ready`, `active`, `completed`, `failed`, `progress`,
|
|
110
|
+
`drained`, `error`, `closed`), pause/resume, graceful `close()` (also a
|
|
111
|
+
context manager), automatic lock heartbeats (jobs longer than the lock TTL
|
|
112
|
+
survive), `UnrecoverableError` to skip retries, opt-in ACK batching
|
|
113
|
+
(`ack_batch={"max_size": 50, "max_delay_ms": 5}`: successful ACKs flush as
|
|
114
|
+
one `ACKB` on size/delay/close; a job stays active until its batch settles)
|
|
115
|
+
- **FlowProducer**: `add` (parent/child trees), `add_bulk`, `add_chain`
|
|
116
|
+
(sequential), `add_bulk_then` (fan-in), `get_flow`, atomic rollback
|
|
117
|
+
- **Simple Mode** (`Bunqueue`): Queue + Worker in one object — routes,
|
|
118
|
+
onion middleware, in-process retry (fixed/exponential/jitter/fibonacci/
|
|
119
|
+
custom + `retry_if`), circuit breaker, batch accumulation, event triggers,
|
|
120
|
+
job TTL, priority aging, cooperative cancellation (`get_signal`),
|
|
121
|
+
dedup/debounce defaults, cron shorthands — 1:1 with the official client
|
|
122
|
+
(TCP mode; `embedded` raises)
|
|
123
|
+
- **Connection**: auth token, TLS (`tls=True`, `{"ca_file": ...}`,
|
|
124
|
+
`{"verify": False}`), pipelining, lazy reconnect, structured telemetry
|
|
125
|
+
|
|
126
|
+
Not applicable outside Bun (by design): embedded mode, sandboxed workers,
|
|
127
|
+
`QueueEvents` (in-process subscription; use webhooks or SSE/WS on the HTTP
|
|
128
|
+
port instead).
|
|
129
|
+
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from bunqueue import (
|
|
134
|
+
BunqueueError, # base
|
|
135
|
+
ConnectionClosedError,
|
|
136
|
+
CommandTimeoutError,
|
|
137
|
+
CommandError, # server answered ok=false
|
|
138
|
+
AuthError,
|
|
139
|
+
SerializationError, # payload not msgpack-serializable (e.g. datetime)
|
|
140
|
+
UnrecoverableError, # raise in a processor: fail terminally, no retries
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The SDK logs its otherwise-silent failure points (swallowed command errors,
|
|
145
|
+
raising listeners, failed registrations) at warning level on the `bunqueue`
|
|
146
|
+
logger; attach a handler to see them.
|
|
147
|
+
|
|
148
|
+
### Structured telemetry
|
|
149
|
+
|
|
150
|
+
Pass `on_telemetry` to `Connection` or `Queue` to bridge transport metrics to
|
|
151
|
+
OpenTelemetry, Prometheus, or your logger without adding an SDK dependency:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
queue = Queue("emails", on_telemetry=lambda event: metrics.record(event))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Events cover `connect`, `disconnect`, `reconnect_scheduled`, `auth`, `command`,
|
|
158
|
+
`command_timeout`, and `error`. Command events contain the command name,
|
|
159
|
+
request id, outcome, and monotonic duration. Payloads and authentication tokens
|
|
160
|
+
are never included. A telemetry callback that raises is isolated from the
|
|
161
|
+
transport and cannot fail a queue operation.
|
|
162
|
+
|
|
163
|
+
## Protocol notes
|
|
164
|
+
|
|
165
|
+
- Wire: 4-byte big-endian length prefix + standard msgpack map per message;
|
|
166
|
+
requests carry a `reqId` echoed by the server (pipelining).
|
|
167
|
+
- Integers outside int32 are sent as float64 (exact ≤ 2^53): the server's
|
|
168
|
+
msgpack decoder turns int64 into `BigInt`, which its arithmetic rejects.
|
|
169
|
+
The SDK handles this automatically. Consequence: integers larger than 2^53
|
|
170
|
+
(e.g. 64-bit snowflake IDs) lose precision — pass them as **strings**.
|
|
171
|
+
- Command maps require string keys at every nesting level. Cyclic containers
|
|
172
|
+
and integers outside the float64 range raise `SerializationError` locally,
|
|
173
|
+
before a pending request is registered or any bytes are written. Binary
|
|
174
|
+
values and array-like lists/tuples are preserved by the wire normalizer.
|
|
175
|
+
|
|
176
|
+
## Tests
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python -m venv .venv && .venv/bin/pip install msgpack
|
|
180
|
+
.venv/bin/python tests/test_integration.py # basic (8)
|
|
181
|
+
.venv/bin/python tests/run_e2e.py # full e2e vs real server (112)
|
|
182
|
+
BUNQUEUE_SDK_SOAK_SECONDS=3600 .venv/bin/python tests/soak.py
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Both spawn a real bunqueue server (`bun src/main.ts` from the repo root). The
|
|
186
|
+
E2E runner emits a monotonic duration for each case so the isolated SDK gate
|
|
187
|
+
can rank slow tests without relying on timing assertions. Hardening covers
|
|
188
|
+
independent-connection idempotency and single-lease contention, fixed-seed
|
|
189
|
+
generated payloads, malformed mutation corpora, 1000-job bursts, and
|
|
190
|
+
crash/restart recovery. The opt-in soak keeps one connection alive; set
|
|
191
|
+
`BUNQUEUE_SDK_SOAK_BATCH` to increase load.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT. See the [LICENSE](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE) file.
|
|
196
|
+
Documentation: [bunqueue.dev/guide/sdks](https://bunqueue.dev/guide/sdks/).
|
|
197
|
+
Issues and feature requests: [GitHub issues](https://github.com/egeominotti/bunqueue/issues).
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://bunqueue.dev">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/egeominotti/bunqueue/main/.github/logo.png" alt="bunqueue logo" width="110" />
|
|
5
|
+
</a>
|
|
6
|
+
|
|
7
|
+
# bunqueue-client (Python)
|
|
8
|
+
|
|
9
|
+
**The official Python client for [bunqueue](https://bunqueue.dev), the high performance job queue server.**
|
|
10
|
+
|
|
11
|
+
Native TCP protocol (msgpack, pipelined), one runtime dependency (`msgpack`), sync plus thread based workers.
|
|
12
|
+
|
|
13
|
+
[](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE)
|
|
14
|
+
[](https://github.com/egeominotti/bunqueue/tree/main/sdk/python)
|
|
15
|
+
[](https://github.com/egeominotti/bunqueue/tree/main/sdk/conformance)
|
|
16
|
+
|
|
17
|
+
[Documentation](https://bunqueue.dev/guide/sdks/) · [Protocol spec](https://github.com/egeominotti/bunqueue/blob/main/docs/protocol.md) · [Server](https://github.com/egeominotti/bunqueue) · [Changelog](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/CHANGELOG.md)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Python client for [bunqueue](https://github.com/egeominotti/bunqueue), the
|
|
24
|
+
high-performance job queue server for Bun. Talks the native TCP protocol
|
|
25
|
+
(msgpack, pipelined) with the same core API as the TypeScript SDK, including
|
|
26
|
+
opt-in ACK batching and dependency-free structured transport telemetry.
|
|
27
|
+
Connection pooling remains TypeScript-only for now.
|
|
28
|
+
|
|
29
|
+
The bunqueue **server** runs on Bun (or as a compiled binary / Docker). This
|
|
30
|
+
SDK lets any Python service produce and consume jobs on it: *one queue, any
|
|
31
|
+
language*.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# PyPI release coming soon; install from the repo today:
|
|
37
|
+
pip install "git+https://github.com/egeominotti/bunqueue.git#subdirectory=sdk/python"
|
|
38
|
+
# dependency: msgpack; import name: bunqueue
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python ≥ 3.9 and a running bunqueue server (`bunx bunqueue start`).
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from bunqueue import Queue, Worker
|
|
47
|
+
|
|
48
|
+
# Producer
|
|
49
|
+
queue = Queue("emails", host="localhost", port=6789)
|
|
50
|
+
job = queue.add("send", {"to": "user@example.com"}, priority=5, attempts=3)
|
|
51
|
+
print(job.id)
|
|
52
|
+
|
|
53
|
+
# Consumer
|
|
54
|
+
def process(job):
|
|
55
|
+
job.update_progress(50)
|
|
56
|
+
return {"sent": True}
|
|
57
|
+
|
|
58
|
+
worker = Worker("emails", process, concurrency=10)
|
|
59
|
+
worker.on("completed", lambda job, result: print(job.id, result))
|
|
60
|
+
worker.run() # blocking; or worker.start() for a background thread
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
All queue semantics (retry, backoff, DLQ, priorities, stall detection, cron)
|
|
64
|
+
run **server-side** — the worker only pulls, heartbeats, and acks.
|
|
65
|
+
|
|
66
|
+
## Feature surface
|
|
67
|
+
|
|
68
|
+
- **Queue**: `add`, `add_bulk`, full job options (priority, delay, attempts,
|
|
69
|
+
backoff, ttl, timeout, job_id, deduplication, depends_on, tags, group_id,
|
|
70
|
+
lifo, remove_on_complete/fail, durable, repeat, debounce, …)
|
|
71
|
+
- **Query**: `get_job`, `get_job_by_custom_id`, `get_jobs` (+ per-state
|
|
72
|
+
helpers), `get_state`, `get_result`, `get_progress`, `wait_for_job`
|
|
73
|
+
(raises `CommandTimeoutError` on timeout, BullMQ contract),
|
|
74
|
+
counts (+ per-priority), children values, job logs
|
|
75
|
+
- **Control**: pause/resume/drain/obliterate/clean, remove, discard,
|
|
76
|
+
promote (single/bulk), retry_job / retry_jobs, move to wait/delayed,
|
|
77
|
+
change priority/delay, update data, extend lock
|
|
78
|
+
- **DLQ**: `get_dlq`, `retry_dlq`, `purge_dlq`, DLQ config
|
|
79
|
+
- **Schedulers**: `upsert_job_scheduler` (cron pattern or interval),
|
|
80
|
+
`add_cron` / `every` shorthands, get/list/remove
|
|
81
|
+
- **Admin**: rate limit, global concurrency, stall config, webhooks,
|
|
82
|
+
stats/metrics/list_queues/get_workers
|
|
83
|
+
- **Worker**: events (`ready`, `active`, `completed`, `failed`, `progress`,
|
|
84
|
+
`drained`, `error`, `closed`), pause/resume, graceful `close()` (also a
|
|
85
|
+
context manager), automatic lock heartbeats (jobs longer than the lock TTL
|
|
86
|
+
survive), `UnrecoverableError` to skip retries, opt-in ACK batching
|
|
87
|
+
(`ack_batch={"max_size": 50, "max_delay_ms": 5}`: successful ACKs flush as
|
|
88
|
+
one `ACKB` on size/delay/close; a job stays active until its batch settles)
|
|
89
|
+
- **FlowProducer**: `add` (parent/child trees), `add_bulk`, `add_chain`
|
|
90
|
+
(sequential), `add_bulk_then` (fan-in), `get_flow`, atomic rollback
|
|
91
|
+
- **Simple Mode** (`Bunqueue`): Queue + Worker in one object — routes,
|
|
92
|
+
onion middleware, in-process retry (fixed/exponential/jitter/fibonacci/
|
|
93
|
+
custom + `retry_if`), circuit breaker, batch accumulation, event triggers,
|
|
94
|
+
job TTL, priority aging, cooperative cancellation (`get_signal`),
|
|
95
|
+
dedup/debounce defaults, cron shorthands — 1:1 with the official client
|
|
96
|
+
(TCP mode; `embedded` raises)
|
|
97
|
+
- **Connection**: auth token, TLS (`tls=True`, `{"ca_file": ...}`,
|
|
98
|
+
`{"verify": False}`), pipelining, lazy reconnect, structured telemetry
|
|
99
|
+
|
|
100
|
+
Not applicable outside Bun (by design): embedded mode, sandboxed workers,
|
|
101
|
+
`QueueEvents` (in-process subscription; use webhooks or SSE/WS on the HTTP
|
|
102
|
+
port instead).
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from bunqueue import (
|
|
108
|
+
BunqueueError, # base
|
|
109
|
+
ConnectionClosedError,
|
|
110
|
+
CommandTimeoutError,
|
|
111
|
+
CommandError, # server answered ok=false
|
|
112
|
+
AuthError,
|
|
113
|
+
SerializationError, # payload not msgpack-serializable (e.g. datetime)
|
|
114
|
+
UnrecoverableError, # raise in a processor: fail terminally, no retries
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The SDK logs its otherwise-silent failure points (swallowed command errors,
|
|
119
|
+
raising listeners, failed registrations) at warning level on the `bunqueue`
|
|
120
|
+
logger; attach a handler to see them.
|
|
121
|
+
|
|
122
|
+
### Structured telemetry
|
|
123
|
+
|
|
124
|
+
Pass `on_telemetry` to `Connection` or `Queue` to bridge transport metrics to
|
|
125
|
+
OpenTelemetry, Prometheus, or your logger without adding an SDK dependency:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
queue = Queue("emails", on_telemetry=lambda event: metrics.record(event))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Events cover `connect`, `disconnect`, `reconnect_scheduled`, `auth`, `command`,
|
|
132
|
+
`command_timeout`, and `error`. Command events contain the command name,
|
|
133
|
+
request id, outcome, and monotonic duration. Payloads and authentication tokens
|
|
134
|
+
are never included. A telemetry callback that raises is isolated from the
|
|
135
|
+
transport and cannot fail a queue operation.
|
|
136
|
+
|
|
137
|
+
## Protocol notes
|
|
138
|
+
|
|
139
|
+
- Wire: 4-byte big-endian length prefix + standard msgpack map per message;
|
|
140
|
+
requests carry a `reqId` echoed by the server (pipelining).
|
|
141
|
+
- Integers outside int32 are sent as float64 (exact ≤ 2^53): the server's
|
|
142
|
+
msgpack decoder turns int64 into `BigInt`, which its arithmetic rejects.
|
|
143
|
+
The SDK handles this automatically. Consequence: integers larger than 2^53
|
|
144
|
+
(e.g. 64-bit snowflake IDs) lose precision — pass them as **strings**.
|
|
145
|
+
- Command maps require string keys at every nesting level. Cyclic containers
|
|
146
|
+
and integers outside the float64 range raise `SerializationError` locally,
|
|
147
|
+
before a pending request is registered or any bytes are written. Binary
|
|
148
|
+
values and array-like lists/tuples are preserved by the wire normalizer.
|
|
149
|
+
|
|
150
|
+
## Tests
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python -m venv .venv && .venv/bin/pip install msgpack
|
|
154
|
+
.venv/bin/python tests/test_integration.py # basic (8)
|
|
155
|
+
.venv/bin/python tests/run_e2e.py # full e2e vs real server (112)
|
|
156
|
+
BUNQUEUE_SDK_SOAK_SECONDS=3600 .venv/bin/python tests/soak.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Both spawn a real bunqueue server (`bun src/main.ts` from the repo root). The
|
|
160
|
+
E2E runner emits a monotonic duration for each case so the isolated SDK gate
|
|
161
|
+
can rank slow tests without relying on timing assertions. Hardening covers
|
|
162
|
+
independent-connection idempotency and single-lease contention, fixed-seed
|
|
163
|
+
generated payloads, malformed mutation corpora, 1000-job bursts, and
|
|
164
|
+
crash/restart recovery. The opt-in soak keeps one connection alive; set
|
|
165
|
+
`BUNQUEUE_SDK_SOAK_BATCH` to increase load.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT. See the [LICENSE](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE) file.
|
|
170
|
+
Documentation: [bunqueue.dev/guide/sdks](https://bunqueue.dev/guide/sdks/).
|
|
171
|
+
Issues and feature requests: [GitHub issues](https://github.com/egeominotti/bunqueue/issues).
|