Skip to content

Commit 2fd0820

Browse files
committed
Rethink the CLI commands
There are two main sections. One for doing the main operations, monitoring and running invocations, and the other for describing command related to entities. Create a new sync command that ensures the queues are on the broker, and don't auto-create queues. A future iteration will allow for creating a lockfile and also be able to drop unneeded queues from the broker.
1 parent b6fb446 commit 2fd0820

11 files changed

Lines changed: 109 additions & 21 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ to allow a project to be deployed in multiple environments.
5858
QUEUEIO_PIKA='amqp://guest:guest@localhost:5672/'
5959
```
6060

61+
Sync the queues to the broker:
62+
63+
```python
64+
queueio sync
65+
```
66+
6167
Submit the routine to run on a worker:
6268

6369
```python
@@ -71,7 +77,7 @@ with activate():
7177
Then run the worker to process submitted routines:
7278

7379
```sh
74-
queueio worker queueio=3
80+
queueio run queueio=3
7581
```
7682

7783
Monitor the status of active routine invocations:

queueio/__main__.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,25 @@
1010

1111
app = Typer()
1212

13-
14-
@app.command()
15-
def routines():
13+
routine_app = Typer()
14+
queue_app = Typer()
15+
16+
app.add_typer(
17+
routine_app,
18+
name="routine",
19+
help="A function to coordinate background execution.",
20+
rich_help_panel="Entities",
21+
)
22+
app.add_typer(
23+
queue_app,
24+
name="queue",
25+
help="An ordered collection of work items to process.",
26+
rich_help_panel="Entities",
27+
)
28+
29+
30+
@routine_app.command("list")
31+
def routine_list():
1632
"""Show all registered routines."""
1733
queueio = QueueIO()
1834
try:
@@ -39,11 +55,11 @@ def routines():
3955
queueio.shutdown()
4056

4157

42-
@app.command()
58+
@app.command(rich_help_panel="Commands")
4359
def monitor(raw: bool = False):
4460
"""Monitor queueio events.
4561
46-
Shows a live view of queueio activity. Use --raw for detailed event output.
62+
Show a live view of queueio activity. Use --raw for detailed event output.
4763
"""
4864
if raw:
4965
queueio = QueueIO()
@@ -59,8 +75,8 @@ def monitor(raw: bool = False):
5975
Monitor().run()
6076

6177

62-
@app.command()
63-
def worker(
78+
@app.command(rich_help_panel="Commands")
79+
def run(
6480
queuespec: Annotated[
6581
QueueSpec,
6682
Argument(
@@ -71,7 +87,7 @@ def worker(
7187
),
7288
],
7389
):
74-
"""Start a worker to process from a queue.
90+
"""Run a worker to process from a queue.
7591
7692
The worker will process invocations from the specified queue,
7793
as many at a time as specified by the concurrency.
@@ -80,8 +96,31 @@ def worker(
8096
Worker(queueio, queuespec)()
8197

8298

83-
@app.command()
84-
def purge(
99+
@app.command(rich_help_panel="Commands")
100+
def sync():
101+
"""Sync known queues to the broker."""
102+
queueio = QueueIO()
103+
try:
104+
routines = queueio.routines()
105+
106+
if not routines:
107+
print("No routines registered.")
108+
return
109+
110+
queues = sorted({routine.queue for routine in routines})
111+
112+
print(f"Syncing queues for {len(routines)} routine(s):")
113+
for queue in queues:
114+
print(f" Ensuring queue exists: {queue}")
115+
queueio.create(queue=queue)
116+
117+
print(f"Successfully synced {len(queues)} queue(s)")
118+
finally:
119+
queueio.shutdown()
120+
121+
122+
@queue_app.command("purge")
123+
def queue_purge(
85124
queues: Annotated[
86125
str,
87126
Argument(

queueio/broker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def enqueue(self, body: bytes, /, *, queue: str):
2424
"""Enqueue a message."""
2525
raise NotImplementedError("Subclasses must implement this method.")
2626

27+
@abstractmethod
28+
def create(self, *, queue: str):
29+
"""Create a queue if it doesn't exist."""
30+
raise NotImplementedError("Subclasses must implement this method.")
31+
2732
@abstractmethod
2833
def purge(self, *, queue: str):
2934
"""Purge all messages from the queue."""

queueio/broker_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def broker(self):
6060
@pytest.mark.timeout(2)
6161
def test_prefetch_limits_message_consumption(self, broker):
6262
"""Verify that prefetch parameter limits message consumption."""
63+
broker.create(queue="test-queue")
6364
broker.purge(queue="test-queue")
6465

6566
# Enqueue more messages than prefetch limit
@@ -92,6 +93,7 @@ def receive_messages():
9293
@pytest.mark.timeout(2)
9394
def test_suspend_resume_affects_prefetch_capacity(self, broker):
9495
"""Verify suspending messages frees capacity and resuming reduces it."""
96+
broker.create(queue="test-queue")
9597
broker.purge(queue="test-queue")
9698

9799
# Enqueue messages
@@ -129,6 +131,7 @@ def receive_messages():
129131
@pytest.mark.timeout(2)
130132
def test_complete_message_frees_prefetch_capacity(self, broker):
131133
"""Verify completing messages frees up capacity."""
134+
broker.create(queue="test-queue")
132135
broker.purge(queue="test-queue")
133136

134137
# Enqueue more messages than prefetch limit
@@ -168,6 +171,7 @@ def receive_messages():
168171
@pytest.mark.timeout(2)
169172
def test_multiple_receivers_independent_prefetch_limits(self, broker):
170173
"""Verify multiple receivers operate with independent prefetch limits."""
174+
broker.create(queue="test-queue")
171175
broker.purge(queue="test-queue")
172176

173177
# Enqueue enough messages for both receivers
@@ -219,6 +223,8 @@ def test_receive_rejects_empty_queues(self, broker):
219223
@skip_if_unsupported("supports_multiple_queues")
220224
def test_receive_supports_multiple_queues(self, broker):
221225
"""Verify broker can receive from multiple queues."""
226+
broker.create(queue="queue1")
227+
broker.create(queue="queue2")
222228
broker.purge(queue="queue1")
223229
broker.purge(queue="queue2")
224230

@@ -254,6 +260,10 @@ def receive_messages():
254260
def test_multiple_queues_with_mixed_empty_and_filled(self, broker):
255261
"""Verify broker handles multiple queues with some empty, some with messages."""
256262

263+
broker.create(queue="empty1")
264+
broker.create(queue="filled")
265+
broker.create(queue="empty2")
266+
broker.create(queue="also_filled")
257267
broker.purge(queue="empty1")
258268
broker.purge(queue="filled")
259269
broker.purge(queue="empty2")
@@ -298,6 +308,8 @@ def test_duplicate_queue_names_get_additional_priority(self, broker):
298308
probability in the round-robin selection.
299309
"""
300310

311+
broker.create(queue="priority_queue")
312+
broker.create(queue="normal_queue")
301313
broker.purge(queue="priority_queue")
302314
broker.purge(queue="normal_queue")
303315

@@ -398,6 +410,9 @@ def test_empty_queue_cycling_fairness(self, broker):
398410
the message, not just cycle by a fixed amount.
399411
"""
400412

413+
broker.create(queue="empty")
414+
broker.create(queue="queue1")
415+
broker.create(queue="queue2")
401416
broker.purge(queue="empty")
402417
broker.purge(queue="queue1")
403418
broker.purge(queue="queue2")

queueio/pika/broker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ def __init__(self, connection_params: Parameters):
2626
self.__receivers = set[PikaReceiver]()
2727

2828
def enqueue(self, body: bytes, /, *, queue: str):
29-
self.__channel.declare_queue(queue=queue, durable=True)
3029
self.__channel.publish(exchange="", routing_key=queue, body=body)
3130

32-
def purge(self, *, queue: str):
31+
def create(self, *, queue: str):
3332
self.__channel.declare_queue(queue=queue, durable=True)
33+
34+
def purge(self, *, queue: str):
3435
self.__channel.purge(queue=queue)
3536

3637
def receive(self, queuespec: QueueSpec, /) -> PikaReceiver:

queueio/pika/receiver.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ def __init__(
2727
self.__prefetch = 0
2828
self.__adjust_prefetch(+queuespec.concurrency)
2929

30-
declared_queues = set()
3130
for queue in queuespec.queues:
32-
if queue not in declared_queues:
33-
self.__channel.declare_queue(queue=queue, durable=True)
34-
declared_queues.add(queue)
3531
result = self.__channel.consume(queue)
3632
self.__consumer_tag[queue] = cast(str, result.method.consumer_tag)
3733

queueio/queueio.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ def run[R](self, invocation: Invocation[R], /) -> R:
112112
with self.invocation_handler():
113113
return invocation.submit().result()
114114

115+
def create(self, *, queue: str):
116+
self.__broker.create(queue=queue)
117+
115118
def purge(self, *, queue: str):
116119
self.__broker.purge(queue=queue)
117120

queueio/queueio_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def test_queueio_with_custom_broker_and_journal():
1717

1818
try:
1919
# Test purge (uses broker)
20+
queueio.create(queue="queueio")
2021
queueio.purge(queue="queueio")
2122

2223
# Test subscriptions (uses journal)
@@ -39,7 +40,9 @@ def test_different_queueio_instances_are_independent():
3940

4041
try:
4142
# Both should work independently
43+
queueio1.create(queue="queueio")
4244
queueio1.purge(queue="queueio")
45+
queueio2.create(queue="queueio")
4346
queueio2.purge(queue="queueio")
4447

4548
# Test that they can have independent subscriptions
@@ -83,6 +86,7 @@ def test_queueio_loads_configuration_from_pyproject(tmp_path):
8386
queueio = QueueIO()
8487
try:
8588
# Should be able to perform basic operations
89+
queueio.create(queue="test")
8690
queueio.purge(queue="test")
8791
events = queueio.subscribe({object})
8892
queueio.unsubscribe(events)
@@ -132,7 +136,9 @@ def test_queueio_allows_pika_override(tmp_path):
132136

133137
try:
134138
# Both should work
139+
queueio1.create(queue="test")
135140
queueio1.purge(queue="test")
141+
queueio2.create(queue="test")
136142
queueio2.purge(queue="test")
137143
finally:
138144
queueio1.shutdown()
@@ -203,6 +209,7 @@ def test_queueio_with_valid_config(tmp_path):
203209
queueio = QueueIO()
204210
try:
205211
# Should load configuration successfully
212+
queueio.create(queue="test")
206213
queueio.purge(queue="test")
207214
routines = queueio.routines()
208215
routine_names = {routine.name for routine in routines}
@@ -320,6 +327,7 @@ def test_queueio_with_uri_broker_config(tmp_path):
320327
queueio = QueueIO()
321328
try:
322329
# Should work with URI configuration
330+
queueio.create(queue="test")
323331
queueio.purge(queue="test")
324332
finally:
325333
queueio.shutdown()
@@ -359,6 +367,7 @@ def test_queueio_with_uri_journal_config(tmp_path):
359367
queueio = QueueIO()
360368
try:
361369
# Should work with URI configuration
370+
queueio.create(queue="test")
362371
queueio.purge(queue="test")
363372
finally:
364373
queueio.shutdown()
@@ -398,6 +407,7 @@ def test_queueio_with_both_uri_configs(tmp_path):
398407
queueio = QueueIO()
399408
try:
400409
# Should work with both URI configurations
410+
queueio.create(queue="test")
401411
queueio.purge(queue="test")
402412
finally:
403413
queueio.shutdown()
@@ -478,6 +488,7 @@ def test_queueio_with_environment_variable(tmp_path, monkeypatch):
478488
queueio = QueueIO()
479489
try:
480490
# Should work with environment variables taking precedence
491+
queueio.create(queue="test")
481492
queueio.purge(queue="test")
482493
finally:
483494
queueio.shutdown()

queueio/samples/basic_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ def test_integration():
1414
queueio = QueueIO()
1515

1616
with queueio.activate():
17+
queueio.create(queue="queueio")
1718
queueio.purge(queue="queueio")
1819
events = queueio.subscribe({Invocation.Completed})
1920
invocation = yielding(7)
2021
queueio.submit(invocation)
2122

2223
proc = subprocess.Popen(
23-
[sys.executable, "-m", "queueio", "worker", "queueio=1"],
24+
[sys.executable, "-m", "queueio", "run", "queueio=1"],
2425
stdout=subprocess.PIPE,
2526
stderr=subprocess.PIPE,
2627
)

queueio/samples/expanded_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ def test_integration():
1515
queueio = QueueIO()
1616

1717
with queueio.activate():
18+
queueio.create(queue="queueio")
1819
queueio.purge(queue="queueio")
1920
events = queueio.subscribe({Invocation.Completed})
2021
invocation = irregular()
2122
queueio.submit(invocation)
2223

2324
# 1. Start worker process in the background
2425
proc = subprocess.Popen(
25-
[sys.executable, "-m", "queueio", "worker", "queueio=1"],
26+
[sys.executable, "-m", "queueio", "run", "queueio=1"],
2627
stdout=subprocess.PIPE,
2728
stderr=subprocess.PIPE,
2829
)

0 commit comments

Comments
 (0)