nodus-queue 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shawn Knight
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,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: nodus-queue
3
+ Version: 0.1.0
4
+ Summary: Distributed job queue with DLQ, delayed jobs, in-flight tracking, Redis backend, and in-memory fallback
5
+ Author: Shawn Knight
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Masterplanner25/nodus-queue
8
+ Project-URL: Repository, https://github.com/Masterplanner25/nodus-queue
9
+ Keywords: queue,redis,distributed,jobs,dlq,nodus
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: tenacity>=8.0.0
19
+ Provides-Extra: redis
20
+ Requires-Dist: redis>=4.0.0; extra == "redis"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Requires-Dist: fakeredis>=2.0.0; extra == "dev"
24
+ Requires-Dist: redis>=4.0.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # nodus-queue
28
+
29
+ Distributed job queue with Dead Letter Queue, delayed jobs, in-flight tracking, and visibility-timeout recovery. Redis backend for multi-instance production; in-memory fallback for dev and tests. Zero hard dependencies beyond `tenacity`.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install nodus-queue # core + in-memory backend
35
+ pip install "nodus-queue[redis]" # + Redis backend
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ from nodus_queue import QueueJobPayload, get_queue, reset_queue
42
+
43
+ # In dev/test — in-memory backend (automatic when REDIS_URL is unset)
44
+ q = get_queue()
45
+
46
+ job = QueueJobPayload(job_id="run-123", task_name="agent.run")
47
+ q.enqueue(job)
48
+
49
+ # Worker side
50
+ job = q.dequeue(timeout=5) # blocks up to 5 seconds
51
+ if job:
52
+ try:
53
+ # ... process job ...
54
+ q.ack(job.job_id) # remove from in-flight
55
+ except Exception as e:
56
+ q.fail(job.job_id, str(e)) # move to DLQ
57
+ ```
58
+
59
+ ## Redis backend
60
+
61
+ ```bash
62
+ REDIS_URL=redis://localhost:6379/0
63
+ ```
64
+
65
+ ```python
66
+ from nodus_queue import get_queue
67
+
68
+ q = get_queue() # picks up REDIS_URL automatically
69
+ ```
70
+
71
+ ## Delayed jobs
72
+
73
+ ```python
74
+ # Schedule a job to run after 30 seconds
75
+ q.enqueue_delayed(job, delay_seconds=30)
76
+
77
+ # Promote ready jobs (call periodically in Redis mode)
78
+ count = q.process_delayed_jobs()
79
+ ```
80
+
81
+ ## Crash recovery
82
+
83
+ ```python
84
+ # On worker startup — re-enqueue jobs stuck in-flight for > 5 minutes
85
+ q.requeue_stale_jobs(timeout_seconds=300)
86
+ ```
87
+
88
+ ## Dead Letter Queue
89
+
90
+ ```python
91
+ depth = q.get_dlq_depth()
92
+ q.drain_dead_letters() # clear all
93
+ q.remove_dead_letter("job-id-123") # remove one
94
+ ```
95
+
96
+ ## Optional Prometheus metrics
97
+
98
+ ```python
99
+ from prometheus_client import CollectorRegistry, Counter, Gauge
100
+ from nodus_queue import QueueMetrics, get_queue
101
+
102
+ REGISTRY = CollectorRegistry()
103
+ enq = Counter("queue_enqueue_total", "...", ["backend", "outcome"], registry=REGISTRY)
104
+
105
+ class MyMetrics(QueueMetrics):
106
+ def on_enqueue(self, backend, outcome):
107
+ enq.labels(backend=backend, outcome=outcome).inc()
108
+ # override other hooks as needed
109
+
110
+ q = get_queue(metrics=MyMetrics())
111
+ ```
112
+
113
+ ## Backend change callback
114
+
115
+ ```python
116
+ def on_change(event: str, payload: dict) -> None:
117
+ print(f"Queue backend changed: {event} {payload}")
118
+
119
+ q = get_queue(on_backend_change=on_change)
120
+ ```
121
+
122
+ ## Environment variables
123
+
124
+ | Variable | Default | Purpose |
125
+ |---|---|---|
126
+ | `REDIS_URL` | — | Redis connection URL |
127
+ | `NODUS_QUEUE_NAME` | `nodus:jobs` | Key prefix for all queue Redis keys |
128
+ | `NODUS_QUEUE_MAXSIZE` | `100` | Hard capacity limit |
129
+ | `NODUS_REQUIRE_REDIS` | `false` | Fail on startup if Redis is unavailable |
130
+ | `EXECUTION_MODE` | `thread` | `distributed` requires `REDIS_URL` |
131
+ | `ENV` | — | `production`/`prod` requires `REDIS_URL` |
132
+ | `TESTING` / `TEST_MODE` | — | Auto-select in-memory backend |
133
+
134
+ ## Extracted from
135
+
136
+ `AINDY/core/distributed_queue.py` in the A.I.N.D.Y. runtime.
@@ -0,0 +1,110 @@
1
+ # nodus-queue
2
+
3
+ Distributed job queue with Dead Letter Queue, delayed jobs, in-flight tracking, and visibility-timeout recovery. Redis backend for multi-instance production; in-memory fallback for dev and tests. Zero hard dependencies beyond `tenacity`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install nodus-queue # core + in-memory backend
9
+ pip install "nodus-queue[redis]" # + Redis backend
10
+ ```
11
+
12
+ ## Quickstart
13
+
14
+ ```python
15
+ from nodus_queue import QueueJobPayload, get_queue, reset_queue
16
+
17
+ # In dev/test — in-memory backend (automatic when REDIS_URL is unset)
18
+ q = get_queue()
19
+
20
+ job = QueueJobPayload(job_id="run-123", task_name="agent.run")
21
+ q.enqueue(job)
22
+
23
+ # Worker side
24
+ job = q.dequeue(timeout=5) # blocks up to 5 seconds
25
+ if job:
26
+ try:
27
+ # ... process job ...
28
+ q.ack(job.job_id) # remove from in-flight
29
+ except Exception as e:
30
+ q.fail(job.job_id, str(e)) # move to DLQ
31
+ ```
32
+
33
+ ## Redis backend
34
+
35
+ ```bash
36
+ REDIS_URL=redis://localhost:6379/0
37
+ ```
38
+
39
+ ```python
40
+ from nodus_queue import get_queue
41
+
42
+ q = get_queue() # picks up REDIS_URL automatically
43
+ ```
44
+
45
+ ## Delayed jobs
46
+
47
+ ```python
48
+ # Schedule a job to run after 30 seconds
49
+ q.enqueue_delayed(job, delay_seconds=30)
50
+
51
+ # Promote ready jobs (call periodically in Redis mode)
52
+ count = q.process_delayed_jobs()
53
+ ```
54
+
55
+ ## Crash recovery
56
+
57
+ ```python
58
+ # On worker startup — re-enqueue jobs stuck in-flight for > 5 minutes
59
+ q.requeue_stale_jobs(timeout_seconds=300)
60
+ ```
61
+
62
+ ## Dead Letter Queue
63
+
64
+ ```python
65
+ depth = q.get_dlq_depth()
66
+ q.drain_dead_letters() # clear all
67
+ q.remove_dead_letter("job-id-123") # remove one
68
+ ```
69
+
70
+ ## Optional Prometheus metrics
71
+
72
+ ```python
73
+ from prometheus_client import CollectorRegistry, Counter, Gauge
74
+ from nodus_queue import QueueMetrics, get_queue
75
+
76
+ REGISTRY = CollectorRegistry()
77
+ enq = Counter("queue_enqueue_total", "...", ["backend", "outcome"], registry=REGISTRY)
78
+
79
+ class MyMetrics(QueueMetrics):
80
+ def on_enqueue(self, backend, outcome):
81
+ enq.labels(backend=backend, outcome=outcome).inc()
82
+ # override other hooks as needed
83
+
84
+ q = get_queue(metrics=MyMetrics())
85
+ ```
86
+
87
+ ## Backend change callback
88
+
89
+ ```python
90
+ def on_change(event: str, payload: dict) -> None:
91
+ print(f"Queue backend changed: {event} {payload}")
92
+
93
+ q = get_queue(on_backend_change=on_change)
94
+ ```
95
+
96
+ ## Environment variables
97
+
98
+ | Variable | Default | Purpose |
99
+ |---|---|---|
100
+ | `REDIS_URL` | — | Redis connection URL |
101
+ | `NODUS_QUEUE_NAME` | `nodus:jobs` | Key prefix for all queue Redis keys |
102
+ | `NODUS_QUEUE_MAXSIZE` | `100` | Hard capacity limit |
103
+ | `NODUS_REQUIRE_REDIS` | `false` | Fail on startup if Redis is unavailable |
104
+ | `EXECUTION_MODE` | `thread` | `distributed` requires `REDIS_URL` |
105
+ | `ENV` | — | `production`/`prod` requires `REDIS_URL` |
106
+ | `TESTING` / `TEST_MODE` | — | Auto-select in-memory backend |
107
+
108
+ ## Extracted from
109
+
110
+ `AINDY/core/distributed_queue.py` in the A.I.N.D.Y. runtime.
@@ -0,0 +1,58 @@
1
+ """nodus-queue — distributed job queue with DLQ, delayed jobs, and in-flight tracking.
2
+
3
+ Backends:
4
+ RedisQueueBackend — LPUSH/BRPOP; single-consumer atomic; Lua-script capacity guard
5
+ InMemoryQueueBackend — thread-safe; Timer-based delayed enqueue; for tests and dev
6
+
7
+ Payload:
8
+ QueueJobPayload — serialisable job envelope with idempotency key
9
+
10
+ Metrics hook:
11
+ QueueMetrics — optional noop base class; subclass to wire Prometheus
12
+
13
+ Errors:
14
+ QueueSaturatedError — raised when the queue rejects work at capacity
15
+
16
+ Factory:
17
+ get_queue() — return the singleton backend (Redis or in-memory fallback)
18
+ reset_queue() — reset singleton for test isolation
19
+ validate_queue_backend() — fail fast if backend is unavailable
20
+ get_queue_health_snapshot() — health dict for monitoring
21
+ attempt_queue_backend_reconnect() — try to restore Redis after degraded fallback
22
+ """
23
+ from .backends import (
24
+ QUEUE_NAME_DEFAULT,
25
+ DistributedQueueBackend,
26
+ InMemoryQueueBackend,
27
+ QueueSaturatedError,
28
+ RedisQueueBackend,
29
+ )
30
+ from .metrics import QueueMetrics
31
+ from .payload import QueueJobPayload
32
+ from .queue import (
33
+ attempt_queue_backend_reconnect,
34
+ get_queue,
35
+ get_queue_health_snapshot,
36
+ reset_queue,
37
+ validate_queue_backend,
38
+ )
39
+
40
+ __all__ = [
41
+ # Backends
42
+ "DistributedQueueBackend",
43
+ "InMemoryQueueBackend",
44
+ "RedisQueueBackend",
45
+ "QUEUE_NAME_DEFAULT",
46
+ # Payload
47
+ "QueueJobPayload",
48
+ # Metrics
49
+ "QueueMetrics",
50
+ # Errors
51
+ "QueueSaturatedError",
52
+ # Factory
53
+ "get_queue",
54
+ "reset_queue",
55
+ "validate_queue_backend",
56
+ "get_queue_health_snapshot",
57
+ "attempt_queue_backend_reconnect",
58
+ ]