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.
Files changed (57) hide show
  1. bunqueue_client-0.1.5/.gitignore +5 -0
  2. bunqueue_client-0.1.5/CHANGELOG.md +183 -0
  3. bunqueue_client-0.1.5/CLAUDE.md +89 -0
  4. bunqueue_client-0.1.5/LICENSE +21 -0
  5. bunqueue_client-0.1.5/PKG-INFO +197 -0
  6. bunqueue_client-0.1.5/README.md +171 -0
  7. bunqueue_client-0.1.5/bunqueue/__init__.py +69 -0
  8. bunqueue_client-0.1.5/bunqueue/ack_batcher.py +93 -0
  9. bunqueue_client-0.1.5/bunqueue/connection.py +240 -0
  10. bunqueue_client-0.1.5/bunqueue/connection_lifecycle.py +104 -0
  11. bunqueue_client-0.1.5/bunqueue/errors.py +38 -0
  12. bunqueue_client-0.1.5/bunqueue/events.py +56 -0
  13. bunqueue_client-0.1.5/bunqueue/flow.py +257 -0
  14. bunqueue_client-0.1.5/bunqueue/job.py +179 -0
  15. bunqueue_client-0.1.5/bunqueue/options.py +126 -0
  16. bunqueue_client-0.1.5/bunqueue/py.typed +0 -0
  17. bunqueue_client-0.1.5/bunqueue/queue.py +230 -0
  18. bunqueue_client-0.1.5/bunqueue/queue_admin.py +201 -0
  19. bunqueue_client-0.1.5/bunqueue/queue_query.py +204 -0
  20. bunqueue_client-0.1.5/bunqueue/simple/__init__.py +16 -0
  21. bunqueue_client-0.1.5/bunqueue/simple/aging.py +68 -0
  22. bunqueue_client-0.1.5/bunqueue/simple/app.py +163 -0
  23. bunqueue_client-0.1.5/bunqueue/simple/app_api.py +142 -0
  24. bunqueue_client-0.1.5/bunqueue/simple/batch.py +88 -0
  25. bunqueue_client-0.1.5/bunqueue/simple/cancellation.py +79 -0
  26. bunqueue_client-0.1.5/bunqueue/simple/circuit_breaker.py +93 -0
  27. bunqueue_client-0.1.5/bunqueue/simple/dedup_debounce.py +42 -0
  28. bunqueue_client-0.1.5/bunqueue/simple/rate_gate.py +57 -0
  29. bunqueue_client-0.1.5/bunqueue/simple/retry.py +65 -0
  30. bunqueue_client-0.1.5/bunqueue/simple/triggers.py +45 -0
  31. bunqueue_client-0.1.5/bunqueue/simple/ttl.py +34 -0
  32. bunqueue_client-0.1.5/bunqueue/telemetry.py +29 -0
  33. bunqueue_client-0.1.5/bunqueue/transport.py +50 -0
  34. bunqueue_client-0.1.5/bunqueue/wire.py +99 -0
  35. bunqueue_client-0.1.5/bunqueue/worker.py +259 -0
  36. bunqueue_client-0.1.5/bunqueue/worker_runtime.py +221 -0
  37. bunqueue_client-0.1.5/pyproject.toml +41 -0
  38. bunqueue_client-0.1.5/tests/e2e_admin.py +154 -0
  39. bunqueue_client-0.1.5/tests/e2e_audit_fixes.py +529 -0
  40. bunqueue_client-0.1.5/tests/e2e_auth.py +56 -0
  41. bunqueue_client-0.1.5/tests/e2e_control.py +133 -0
  42. bunqueue_client-0.1.5/tests/e2e_edge.py +223 -0
  43. bunqueue_client-0.1.5/tests/e2e_flow.py +149 -0
  44. bunqueue_client-0.1.5/tests/e2e_hardening.py +125 -0
  45. bunqueue_client-0.1.5/tests/e2e_query.py +110 -0
  46. bunqueue_client-0.1.5/tests/e2e_realistic.py +182 -0
  47. bunqueue_client-0.1.5/tests/e2e_serialization.py +107 -0
  48. bunqueue_client-0.1.5/tests/e2e_simple.py +213 -0
  49. bunqueue_client-0.1.5/tests/e2e_simple_extras.py +179 -0
  50. bunqueue_client-0.1.5/tests/e2e_spec_align.py +147 -0
  51. bunqueue_client-0.1.5/tests/e2e_telemetry.py +50 -0
  52. bunqueue_client-0.1.5/tests/e2e_worker.py +263 -0
  53. bunqueue_client-0.1.5/tests/harness.py +108 -0
  54. bunqueue_client-0.1.5/tests/run_audit.py +27 -0
  55. bunqueue_client-0.1.5/tests/run_e2e.py +48 -0
  56. bunqueue_client-0.1.5/tests/soak.py +63 -0
  57. bunqueue_client-0.1.5/tests/test_integration.py +206 -0
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
@@ -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
+ [![license](https://img.shields.io/badge/license-MIT-1a1a2e)](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE)
40
+ [![python](https://img.shields.io/badge/python-3.9%2B-2ea44f)](https://github.com/egeominotti/bunqueue/tree/main/sdk/python)
41
+ [![conformance](https://img.shields.io/badge/protocol-conformant%2017%2F17-d3156d)](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
+ [![license](https://img.shields.io/badge/license-MIT-1a1a2e)](https://github.com/egeominotti/bunqueue/blob/main/sdk/python/LICENSE)
14
+ [![python](https://img.shields.io/badge/python-3.9%2B-2ea44f)](https://github.com/egeominotti/bunqueue/tree/main/sdk/python)
15
+ [![conformance](https://img.shields.io/badge/protocol-conformant%2017%2F17-d3156d)](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).