Skip to content

Commit 36dccca

Browse files
committed
Some progress on typing
1 parent 030c472 commit 36dccca

File tree

15 files changed

+137
-110
lines changed

15 files changed

+137
-110
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ test-py313:
103103
copy-md:
104104
cp ./README.md docs/source/intro.md
105105
cp ./QUICK_START.md docs/source/quick_start.md
106+
cp ./RECIPES.md docs/source/recipes.md
106107

107108
docs: copy-md
108109
uv run sphinx-build -b html docs/source docs/build/html

QUICK_START.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,15 @@ def worker1(task: mml.main.tasks.TaskMessage) -> None:
179179

180180
def worker2(task: mml.main.tasks.TaskMessage) -> None:
181181
print("2- Working on:", task)
182-
183-
pb.subscribe(mml.main.tasks.TaskMessage, worker1)
182+
import mymessagelib as mml
183+
pb.subscribe(mml.main.tasks.TasqkMessage, worker1)
184184
pb.subscribe(mml.main.tasks.TaskMessage, worker2)
185185

186186
pb.publish(mml.main.tasks.TaskMessage(content="test1"))
187187
pb.publish(mml.main.tasks.TaskMessage(content="test2"))
188188
pb.publish(mml.main.tasks.TaskMessage(content="test3"))
189+
from protobunny.models import ProtoBunnyMessage
190+
print(isinstance(mml.main.tasks.TaskMessage(), ProtoBunnyMessage))
189191
```
190192

191193
You can also introspect/manage an underlying shared queue:

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Protobunny
22

3-
```{warning}
4-
The project is in early development.
5-
```
3+
::: {warning}
4+
**Note**: The project is in early development.
5+
:::
66

77
Protobunny is the open-source evolution of [AM-Flow](https://am-flow.com)'s internal messaging library.
88
While the original was purpose-built for RabbitMQ, this version has been completely re-engineered to provide a unified,
@@ -26,11 +26,11 @@ Supported backends in the current version are:
2626
- Mosquitto
2727
- Python "backend" with Queue/asyncio.Queue for local in-processing testing
2828

29-
```{note}
30-
Protobunny handles backend-specific logic internally to provide a consistent experience and a lean interface.
31-
Direct access to the internal NATS or Redis clients is intentionally restricted.
29+
::: {note}
30+
**Note**: Protobunny handles backend-specific logic internally to provide a consistent experience and a lean interface.
31+
Direct access to the internal NATS or Redis clients is intentionally restricted.
3232
If your project depends on specialized backend parameters not covered by our API, you may find the abstraction too restrictive.
33-
```
33+
:::
3434

3535

3636
## Minimal requirements
@@ -63,8 +63,8 @@ While there are many messaging libraries for Python, Protobunny is built specifi
6363

6464
* **Type-Safe by Design**: Built natively for `protobuf/betterproto`.
6565
* **Semantic Routing**: Zero-config infrastructure. Protobunny uses your Protobuf package structure to decide if a message should be broadcast (Pub/Sub) or queued (Producer/Consumer).
66-
* **Backend Agnostic**: Write your logic once. Switch between Redis, RabbitMQ, Mosquitto, or Local Queues by changing a single variable in configuration.
67-
* **Sync & Async**: Support for both modern `asyncio` and traditional synchronous workloads.
66+
* **Backend Agnostic**: You can choose between RabbitMQ, Redis, NATS, and Mosquitto. Python for local testing.
67+
* **Sync & Async**: Support for both `asyncio` and traditional synchronous workloads.
6868
* **Battle-Tested**: Derived from internal libraries used in production systems at AM-Flow.
6969
---
7070

@@ -78,6 +78,7 @@ While there are many messaging libraries for Python, Protobunny is built specifi
7878
| **Pattern Routing** | ✅ Auto (`tasks` pkg) | ❌ Manual Config | ✅ Fixed |
7979
| **Framework Agnostic** | ✅ Yes | ⚠️ FastAPI-like focus | ❌ Heavyweight |
8080

81+
---
8182

8283
## Usage
8384

RECIPES.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Recipes
2+
3+
4+
## Subscribe to a queue
5+
6+
7+
## Subscribe a task worker to a shared topic
8+
9+
10+
## Publish
11+
12+
13+
## Results workflow
14+
15+
16+
## Requeuing
17+
18+
19+

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"sphinx.ext.viewcode",
2929
"myst_parser",
3030
]
31+
myst_enable_extensions = ["colon_fence"]
3132

3233

3334
templates_path = ["_templates"]

docs/source/intro.md

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
# Protobunny
22

3-
```{warning}
4-
Note: The project is in early development.
5-
```
3+
::: {warning}
4+
**Warning**: The project is in early development.
5+
:::
66

77
Protobunny is the open-source evolution of [AM-Flow](https://am-flow.com)'s internal messaging library.
88
While the original was purpose-built for RabbitMQ, this version has been completely re-engineered to provide a unified,
9-
type-safe interface for several message brokers, including Redis and MQTT.
9+
type-safe interface for several message brokers, including Redis, NATS, and MQTT.
1010

11-
It simplifies messaging for asynchronous tasks by providing:
11+
It simplifies messaging for asynchronous message handling by providing:
1212

13-
* A clean “message-first” API
14-
* Python class generation from Protobuf messages using betterproto
15-
* Connections facilities for backends
13+
* A clean “message-first” API by using your protobuf definitions
1614
* Message publishing/subscribing with typed topics
17-
* Support also “task-like” queues (shared/competing consumers) vs. broadcast subscriptions
15+
* Supports "task-like” queues (shared/competing consumers) vs. broadcast subscriptions
1816
* Generate and consume `Result` messages (success/failure + optional return payload)
1917
* Transparent messages serialization/deserialization
20-
* Support async and sync contexts
21-
* Transparently serialize "JSON-like" payload fields (numpy-friendly)
18+
* Transparently serialize/deserialize custom "JSON-like" payload fields (numpy-friendly)
19+
* Support async and sync contexts
20+
21+
Supported backends in the current version are:
22+
23+
- RabbitMQ
24+
- Redis
25+
- NATS
26+
- Mosquitto
27+
- Python "backend" with Queue/asyncio.Queue for local in-processing testing
28+
29+
::: {note}
30+
**Note**: Protobunny handles backend-specific logic internally to provide a consistent experience and a lean interface.
31+
Direct access to the internal NATS or Redis clients is intentionally restricted.
32+
If your project depends on specialized backend parameters not covered by our API, you may find the abstraction too restrictive.
33+
:::
2234

2335

2436
## Minimal requirements
2537

26-
- Python >= 3.10, < 3.14
38+
- Python >= 3.10 <=3.13
39+
- Core Dependencies: betterproto 2.0.0b7, grpcio-tools>=1.62.0
40+
- Backend Drivers (Optional based on your usage):
41+
- NATS: nats-py (Requires NATS Server v2.10+ for full JetStream support).
42+
- Redis: redis (Requires Redis Server v6.2+ for Stream support).
43+
- RabbitMQ: aio-pika
44+
- Mosquitto: aiomqtt
2745

2846

2947
## Project scope
@@ -45,8 +63,8 @@ While there are many messaging libraries for Python, Protobunny is built specifi
4563

4664
* **Type-Safe by Design**: Built natively for `protobuf/betterproto`.
4765
* **Semantic Routing**: Zero-config infrastructure. Protobunny uses your Protobuf package structure to decide if a message should be broadcast (Pub/Sub) or queued (Producer/Consumer).
48-
* **Backend Agnostic**: Write your logic once. Switch between Redis, RabbitMQ, Mosquitto, or Local Queues by changing a single variable in configuration.
49-
* **Sync & Async**: Support for both modern `asyncio` and traditional synchronous workloads.
66+
* **Backend Agnostic**: You can choose between RabbitMQ, Redis, NATS, and Mosquitto. Python for local testing.
67+
* **Sync & Async**: Support for both `asyncio` and traditional synchronous workloads.
5068
* **Battle-Tested**: Derived from internal libraries used in production systems at AM-Flow.
5169
---
5270

@@ -60,6 +78,7 @@ While there are many messaging libraries for Python, Protobunny is built specifi
6078
| **Pattern Routing** | ✅ Auto (`tasks` pkg) | ❌ Manual Config | ✅ Fixed |
6179
| **Framework Agnostic** | ✅ Yes | ⚠️ FastAPI-like focus | ❌ Heavyweight |
6280

81+
---
6382

6483
## Usage
6584

@@ -74,7 +93,7 @@ Documentation home page: [https://am-flow.github.io/protobunny/](https://am-flow
7493
- [x] **Semantic Patterns**: Automatic `tasks` package routing.
7594
- [x] **Arbistrary dictionary parsing**: Transparently parse JSON-like fields as dictionaries/lists by using protobunny JsonContent type.
7695
- [x] **Result workflow**: Subscribe to results topics and receive protobunny `Result` messages produced by your callbacks.
77-
- [ ] **Cloud-Native**: NATS (Core & JetStream) integration.
96+
- [x] **Cloud-Native**: NATS (Core & JetStream) integration.
7897
- [ ] **Cloud Providers**: AWS (SQS/SNS) and GCP Pub/Sub.
7998
- [ ] **More backends**: Kafka support.
8099

docs/source/quick_start.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ messages-directory = "messages"
3333
messages-prefix = "acme"
3434
generated-package-name = "mymessagelib.codegen"
3535
mode = "async" # or "sync"
36-
backend = "rabbitmq" # available backends are ['rabbitmq', 'redis', 'mosquitto', 'python']
36+
backend = "rabbitmq" # available backends are ['rabbitmq', 'redis', 'nats', 'mosquitto', 'python']
3737
```
3838

3939
### Install the library with `uv`, `poetry` or `pip`
@@ -294,7 +294,7 @@ if conn.is_connected():
294294
conn.close()
295295
```
296296

297-
If you set the `generated-package-root` folder option, you might need to add the path to your `sys.path`.
297+
If you set the `generated-package-root` folder option, you might need to add that path to your `sys.path`.
298298
You can do it conveniently by calling `config_lib` on top of your module, before importing the library:
299299

300300
```python
@@ -429,10 +429,12 @@ class TestLibAsync:
429429
await pb_asyncio.subscribe(ml.tests.TestMessage, self.on_message)
430430
await pb_asyncio.subscribe_results(ml.tests.TestMessage, self.on_message_results)
431431
await pb_asyncio.subscribe(ml.main.MyMessage, self.on_message_mymessage)
432-
432+
433+
# Send a simple message
433434
await pb_asyncio.publish(ml.main.MyMessage(content="test"))
435+
# Send a message with arbitrary json content
434436
await pb_asyncio.publish(ml.tests.TestMessage(number=1, content="test", data={"test": 123}))
435-
437+
# Send three messages to verify the load balancing between the workers
436438
await pb_asyncio.publish(ml.main.tasks.TaskMessage(content="test1"))
437439
await pb_asyncio.publish(ml.main.tasks.TaskMessage(content="test2"))
438440
await pb_asyncio.publish(ml.main.tasks.TaskMessage(content="test3"))
@@ -441,6 +443,7 @@ class TestLibAsync:
441443

442444

443445
class TestLib:
446+
# Sync version
444447
def on_message(self, message: ml.tests.TestMessage) -> None:
445448
log.info("Got: %s", message)
446449
result = message.make_result()

protobunny/__init__.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,7 @@
5353

5454
if tp.TYPE_CHECKING:
5555
from .core.results import Result
56-
from .models import (
57-
IncomingMessageProtocol,
58-
LoggerCallback,
59-
ProtoBunnyMessage,
60-
SyncCallback,
61-
)
56+
from .models import PBM, IncomingMessageProtocol, LoggerCallback, SyncCallback
6257

6358
__version__ = version(PACKAGE_NAME)
6459

@@ -93,7 +88,7 @@ def reset_connection(**kwargs: tp.Any) -> "BaseSyncConnection":
9388
return conn.connect(**kwargs)
9489

9590

96-
def publish(message: "ProtoBunnyMessage") -> None:
91+
def publish(message: "PBM") -> None:
9792
"""Synchronously publish a message to its corresponding queue.
9893
9994
This method automatically determines the correct topic based on the
@@ -122,7 +117,7 @@ def publish_result(
122117

123118

124119
def subscribe(
125-
pkg_or_msg: "type[ProtoBunnyMessage] | ModuleType",
120+
pkg_or_msg: "type[PBM] | ModuleType",
126121
callback: "SyncCallback",
127122
) -> "BaseSyncQueue":
128123
"""Subscribe a callback function to the topic.
@@ -151,7 +146,7 @@ def subscribe(
151146

152147

153148
def subscribe_results(
154-
pkg: "type[ProtoBunnyMessage] | ModuleType",
149+
pkg: "type[PBM] | ModuleType",
155150
callback: "SyncCallback",
156151
) -> "BaseSyncQueue":
157152
"""Subscribe a callback function to the result topic.
@@ -169,7 +164,7 @@ def subscribe_results(
169164

170165

171166
def unsubscribe(
172-
pkg: "type[ProtoBunnyMessage] | ModuleType",
167+
pkg: "type[PBM] | ModuleType",
173168
if_unused: bool = True,
174169
if_empty: bool = True,
175170
) -> None:
@@ -191,7 +186,7 @@ def unsubscribe(
191186

192187

193188
def unsubscribe_results(
194-
pkg: "type[ProtoBunnyMessage] | ModuleType",
189+
pkg: "type[PBM] | ModuleType",
195190
) -> None:
196191
"""Remove all in-process subscriptions for a message/package result topic"""
197192
with registry.sync_lock:
@@ -223,15 +218,15 @@ def unsubscribe_all(if_unused: bool = True, if_empty: bool = True) -> None:
223218

224219

225220
def get_message_count(
226-
msg_type: "ProtoBunnyMessage | type[ProtoBunnyMessage] | ModuleType",
221+
msg_type: "PBM | type[PBM] | ModuleType",
227222
) -> int | None:
228223
q = get_queue(msg_type)
229224
count = q.get_message_count()
230225
return count
231226

232227

233228
def get_consumer_count(
234-
msg_type: "ProtoBunnyMessage | type[ProtoBunnyMessage] | ModuleType",
229+
msg_type: "PBM | type[PBM] | ModuleType",
235230
) -> int | None:
236231
q = get_queue(msg_type)
237232
count = q.get_consumer_count()

protobunny/__init__.py.j2

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ from .helpers import get_backend, get_queue
5656
from .backends import LoggingSyncQueue, BaseSyncQueue, BaseSyncConnection
5757

5858
if tp.TYPE_CHECKING:
59-
from .models import LoggerCallback, ProtoBunnyMessage, SyncCallback, IncomingMessageProtocol
59+
from .models import LoggerCallback, PBM, SyncCallback, IncomingMessageProtocol
6060
from .core.results import Result
6161

6262
__version__ = version(PACKAGE_NAME)
@@ -92,7 +92,7 @@ def reset_connection(**kwargs: tp.Any) -> "BaseSyncConnection":
9292
return conn.connect(**kwargs)
9393

9494

95-
def publish(message: "ProtoBunnyMessage") -> None:
95+
def publish(message: "PBM") -> None:
9696
"""Synchronously publish a message to its corresponding queue.
9797

9898
This method automatically determines the correct topic based on the
@@ -121,7 +121,7 @@ def publish_result(
121121

122122

123123
def subscribe(
124-
pkg_or_msg: "type[ProtoBunnyMessage] | ModuleType",
124+
pkg_or_msg: "type[PBM] | ModuleType",
125125
callback: "SyncCallback",
126126
) -> "BaseSyncQueue":
127127
"""Subscribe a callback function to the topic.
@@ -150,7 +150,7 @@ def subscribe(
150150

151151

152152
def subscribe_results(
153-
pkg: "type[ProtoBunnyMessage] | ModuleType",
153+
pkg: "type[PBM] | ModuleType",
154154
callback: "SyncCallback",
155155
) -> "BaseSyncQueue":
156156
"""Subscribe a callback function to the result topic.
@@ -168,7 +168,7 @@ def subscribe_results(
168168

169169

170170
def unsubscribe(
171-
pkg: "type[ProtoBunnyMessage] | ModuleType",
171+
pkg: "type[PBM] | ModuleType",
172172
if_unused: bool = True,
173173
if_empty: bool = True,
174174
) -> None:
@@ -190,7 +190,7 @@ def unsubscribe(
190190

191191

192192
def unsubscribe_results(
193-
pkg: "type[ProtoBunnyMessage] | ModuleType",
193+
pkg: "type[PBM] | ModuleType",
194194
) -> None:
195195
"""Remove all in-process subscriptions for a message/package result topic"""
196196
with registry.sync_lock:
@@ -222,15 +222,15 @@ def unsubscribe_all(if_unused: bool = True, if_empty: bool = True) -> None:
222222

223223

224224
def get_message_count(
225-
msg_type: "ProtoBunnyMessage | type[ProtoBunnyMessage] | ModuleType",
225+
msg_type: "PBM | type[PBM] | ModuleType",
226226
) -> int | None:
227227
q = get_queue(msg_type)
228228
count = q.get_message_count()
229229
return count
230230

231231

232232
def get_consumer_count(
233-
msg_type: "ProtoBunnyMessage | type[ProtoBunnyMessage] | ModuleType",
233+
msg_type: "PBM | type[PBM] | ModuleType",
234234
) -> int | None:
235235
q = get_queue(msg_type)
236236
count = q.get_consumer_count()

0 commit comments

Comments
 (0)