ropemother 0.1.0.dev8__tar.gz → 0.1.0.dev9__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.
- ropemother-0.1.0.dev9/PKG-INFO +331 -0
- ropemother-0.1.0.dev9/README.md +305 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/pyproject.toml +3 -2
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/buffer.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/policy.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncbase.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncdirect.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncendpoints.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/base.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/direct.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/directcore.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/dispatch.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/endpoints.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/subscription.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/controller.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/filehistory.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/filesink.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/history.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/historyselection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/historyservice.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/jsonrecords.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/memorysink.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/runtime.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/sink.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/writer.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncendpointfactory.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncendpointprovisioner.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncrequest.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/endpointfactory.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/endpointfactorybase.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/lifecycle.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/procedure.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/request.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/requestoptions.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/exceptions.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/fixtures/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/fixtures/scriptedinput.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/defaults.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/formattable.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/portableformat.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/registry.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/messageidentity.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/records.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/registrationtable.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/selectors.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/symbols.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/typeformats.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/playground.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/__main__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/asyncservice.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/broker.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/brokerextension.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/brokerhistory.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/connector.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/descriptor.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/environment.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/host.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/listener.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/resource.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/service.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/socketlistener.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncclient.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncconnection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsession.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsessionrunner.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsocketconnection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/client.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/codec.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/connection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/endpointregistration.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/frames.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/session.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/sessionrunner.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/sessionstate.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/socketconnection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/socketframing.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/address.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/connection.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/__init__.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/compositeblobserializer.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/lengthprefixed.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/onelinejson.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/serializer.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/symbol.py +1 -1
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/typedid.py +1 -1
- ropemother-0.1.0.dev9/ropemother.egg-info/PKG-INFO +331 -0
- ropemother-0.1.0.dev8/PKG-INFO +0 -462
- ropemother-0.1.0.dev8/README.md +0 -437
- ropemother-0.1.0.dev8/ropemother.egg-info/PKG-INFO +0 -462
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/LICENSE +0 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/SOURCES.txt +0 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/dependency_links.txt +0 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/requires.txt +0 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/top_level.txt +0 -0
- {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/setup.cfg +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ropemother
|
|
3
|
+
Version: 0.1.0.dev9
|
|
4
|
+
Summary: A module for publish-subscribe broadcast messaging.
|
|
5
|
+
Author-email: Joe Granville <874605+jwgranville@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/edurange/ropemother
|
|
8
|
+
Project-URL: Issues, https://github.com/edurange/ropemother/issues
|
|
9
|
+
Project-URL: Exercises, https://github.com/edurange/ropemother-exercises
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: zmq
|
|
16
|
+
Requires-Dist: pyzmq; extra == "zmq"
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: black; extra == "dev"
|
|
21
|
+
Requires-Dist: build; extra == "dev"
|
|
22
|
+
Requires-Dist: pylint; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
24
|
+
Requires-Dist: pyzmq; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Ropemother
|
|
28
|
+
|
|
29
|
+
`ropemother` is a Python package for building small message-oriented systems. It provides publish-subscribe messaging, request/reply helpers, capture and history support, portable payload formats, an in-process direct broker, and a freestanding broker for communication between local processes.
|
|
30
|
+
|
|
31
|
+
The current developer release is intended for teaching, research software, local development, and early integration work.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
`ropemother` requires Python 3.13 or newer.
|
|
36
|
+
|
|
37
|
+
Install the current developer release from PyPI:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
python -m pip install --pre ropemother
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The base package has no required third-party runtime dependencies. The exploratory ZeroMQ transport is available as an optional extra:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
python -m pip install --pre "ropemother[zmq]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Publish and subscribe
|
|
50
|
+
|
|
51
|
+
The examples below introduce the main public messaging interfaces with small programs.
|
|
52
|
+
|
|
53
|
+
A direct message bus can route a message from one emitter to every receiver whose subscription matches that message.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
57
|
+
|
|
58
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
59
|
+
|
|
60
|
+
emitter = bus.register_emitter(
|
|
61
|
+
msg_topic="foo-topic.events",
|
|
62
|
+
msg_producer="producer-corge",
|
|
63
|
+
msg_type="type-garply",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
first_receiver = bus.subscribe(
|
|
67
|
+
msg_topic="foo-topic.events",
|
|
68
|
+
msg_producer="producer-corge",
|
|
69
|
+
msg_type="type-garply",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
second_receiver = bus.subscribe(
|
|
73
|
+
msg_topic="foo-topic.events",
|
|
74
|
+
msg_producer="producer-corge",
|
|
75
|
+
msg_type="type-garply",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
emitter.emit("hello from producer")
|
|
79
|
+
|
|
80
|
+
print(first_receiver.receive().payload)
|
|
81
|
+
print(second_receiver.receive().payload)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Expected output:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
hello from producer
|
|
88
|
+
hello from producer
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`msg_topic`, `msg_producer`, and `msg_type` describe the messages that an endpoint emits or receives. In the example, both receivers select messages produced by `producer-corge` on `foo-topic.events` with message type `type-garply`, so both receive the same broadcast message.
|
|
92
|
+
|
|
93
|
+
The direct broker is useful when the participating components can share one Python process. The same endpoint vocabulary is also used with the freestanding broker described below.
|
|
94
|
+
|
|
95
|
+
## Running a freestanding broker
|
|
96
|
+
|
|
97
|
+
A broker coordinates message delivery. The freestanding broker lets separate local processes connect to the same message bus.
|
|
98
|
+
|
|
99
|
+
Start a local broker in one terminal:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
python -m ropemother.service
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The broker prints its connection descriptor and an environment-variable form that client processes can use:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
Message bus broker is running
|
|
109
|
+
broker URI: ropemother+unix:///...
|
|
110
|
+
environment: ROPEMOTHER_CONNECTION_DESCRIPTOR=ropemother+unix:///...
|
|
111
|
+
Press Ctrl-C to stop
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The connection descriptor identifies the broker endpoint that client processes use to find the same message bus.
|
|
115
|
+
|
|
116
|
+
A client can connect using the printed descriptor explicitly:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from ropemother import connect_message_bus
|
|
120
|
+
|
|
121
|
+
bus = connect_message_bus("ropemother+unix:///...")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If `ROPEMOTHER_CONNECTION_DESCRIPTOR` is already set in the environment, the descriptor can be omitted:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from ropemother import connect_message_bus
|
|
128
|
+
|
|
129
|
+
bus = connect_message_bus()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Once connected, client code elsewhere uses the same `register_emitter(...)`, `subscribe(...)`, `emit(...)`, and `receive()` operations as a direct bus.
|
|
133
|
+
|
|
134
|
+
For example, a subscriber process can wait for one message:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from ropemother import connect_message_bus
|
|
138
|
+
|
|
139
|
+
bus = connect_message_bus()
|
|
140
|
+
receiver = bus.subscribe(
|
|
141
|
+
msg_topic="foo-topic.events",
|
|
142
|
+
msg_producer="producer-corge",
|
|
143
|
+
msg_type="type-garply",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
message = receiver.receive()
|
|
147
|
+
print(message.payload)
|
|
148
|
+
|
|
149
|
+
bus.close()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A separate producer process can publish the message:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from ropemother import connect_message_bus
|
|
156
|
+
|
|
157
|
+
bus = connect_message_bus()
|
|
158
|
+
emitter = bus.register_emitter(
|
|
159
|
+
msg_topic="foo-topic.events",
|
|
160
|
+
msg_producer="producer-corge",
|
|
161
|
+
msg_type="type-garply",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
emitter.emit("hello from producer")
|
|
165
|
+
bus.close()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The broker allows the producer, subscriber, and other services to have independent process lifetimes while preserving the same application-facing message model.
|
|
169
|
+
|
|
170
|
+
## Request and reply
|
|
171
|
+
|
|
172
|
+
A request/reply service places an application operation behind a message boundary. The client sends a request and waits for the correlated reply instead of calling the service implementation directly.
|
|
173
|
+
|
|
174
|
+
The following local example uses `str.upper` as the service operation:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import asyncio
|
|
178
|
+
|
|
179
|
+
from ropemother import AsyncDirectMessageBus, InMemoryCaptureSink
|
|
180
|
+
|
|
181
|
+
bus = AsyncDirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
182
|
+
|
|
183
|
+
client = bus.create_procedure_client(
|
|
184
|
+
request_topic="foo-topic.requests",
|
|
185
|
+
reply_topic="foo-topic.replies",
|
|
186
|
+
requester_producer="producer-corge",
|
|
187
|
+
responder_producer="producer-grault",
|
|
188
|
+
request_msg_type="type-garply",
|
|
189
|
+
reply_msg_type="type-waldo",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
service = bus.create_procedure_service(
|
|
193
|
+
request_topic="foo-topic.requests",
|
|
194
|
+
reply_topic="foo-topic.replies",
|
|
195
|
+
requester_producer="producer-corge",
|
|
196
|
+
responder_producer="producer-grault",
|
|
197
|
+
request_msg_type="type-garply",
|
|
198
|
+
reply_msg_type="type-waldo",
|
|
199
|
+
handler=str.upper,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def run_one_request() -> str:
|
|
204
|
+
service_task = asyncio.create_task(service.handle())
|
|
205
|
+
result = await client("hello")
|
|
206
|
+
await service_task
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
print(asyncio.run(run_one_request()))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Expected output:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
HELLO
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
A procedure client is callable and returns the reply payload. `client.call(...)` provides the same payload-returning operation with an explicit method name, while `client.call_reply(...)` returns the full reply message when application code also needs its message metadata.
|
|
220
|
+
|
|
221
|
+
## Capture and history
|
|
222
|
+
|
|
223
|
+
Capture records messages and the information needed to interpret them later.
|
|
224
|
+
|
|
225
|
+
Small in-process applications can supply a capture sink when constructing a direct bus:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
229
|
+
|
|
230
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The freestanding broker enables capture by default and writes JSON Lines records to `.ropemother/capture.jsonl`:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
python -m ropemother.service
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Use `--capture-path` to choose another capture file. Use `--transport-only` only when a local transport experiment explicitly does not need capture, history, or replay guarantees.
|
|
240
|
+
|
|
241
|
+
History is the application-facing way to query captured messages. Start the broker with its built-in history service:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
python -m ropemother.service --history
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Then create the preconfigured client for that service:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from ropemother import connect_message_bus
|
|
251
|
+
from ropemother.service import preconfigured_history_client
|
|
252
|
+
|
|
253
|
+
bus = connect_message_bus()
|
|
254
|
+
|
|
255
|
+
emitter = bus.register_emitter(
|
|
256
|
+
msg_topic="foo-topic.events",
|
|
257
|
+
msg_producer="producer-corge",
|
|
258
|
+
msg_type="type-plugh",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
history_client = preconfigured_history_client(bus)
|
|
262
|
+
|
|
263
|
+
emitter.emit("captured event")
|
|
264
|
+
|
|
265
|
+
page = history_client.select(
|
|
266
|
+
msg_topic="foo-topic.events",
|
|
267
|
+
msg_producer="producer-corge",
|
|
268
|
+
msg_type="type-plugh",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
print(page.entries[0].payload)
|
|
272
|
+
bus.close()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Expected output:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
captured event
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Application code queries the history service through the bus rather than opening the capture file directly. `preconfigured_history_client(...)` supplies the fixed contract used by the broker's built-in history service. Applications that define a different history service can use the explicit history client and service constructors instead.
|
|
282
|
+
|
|
283
|
+
## Portable payloads
|
|
284
|
+
|
|
285
|
+
Portable formats serialize payloads for capture and transport. `ropemother` includes JSON and raw-byte formats and supports project-defined formats for other application data.
|
|
286
|
+
|
|
287
|
+
## Using the source checkout
|
|
288
|
+
|
|
289
|
+
Clone the repository and install its development dependencies into an environment chosen for development work:
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
git clone https://github.com/edurange/ropemother.git
|
|
293
|
+
cd ropemother
|
|
294
|
+
python -m pip install -e ".[dev]"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The source repository is available at:
|
|
298
|
+
|
|
299
|
+
<https://github.com/edurange/ropemother>
|
|
300
|
+
|
|
301
|
+
Issues are tracked at:
|
|
302
|
+
|
|
303
|
+
<https://github.com/edurange/ropemother/issues>
|
|
304
|
+
|
|
305
|
+
See `CONTRIBUTING.md` before preparing a substantial change.
|
|
306
|
+
|
|
307
|
+
### Executable development demos
|
|
308
|
+
|
|
309
|
+
`ropemother/playground.py` contains chronological executable demonstrations used for development, inspection, and smoke checking:
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
python -m ropemother.playground
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The playground is intentionally more verbose than the short examples in this README and is mainly useful for development and inspection.
|
|
316
|
+
|
|
317
|
+
## Supplementary exercises
|
|
318
|
+
|
|
319
|
+
Exercises and tutorial materials are available at:
|
|
320
|
+
|
|
321
|
+
<https://github.com/edurange/ropemother-exercises>
|
|
322
|
+
|
|
323
|
+
## Development status
|
|
324
|
+
|
|
325
|
+
`ropemother` is a developer release. The current implementation covers the local message model, direct and freestanding broker operation, portable payload formats, capture, history queries, and request/reply helpers.
|
|
326
|
+
|
|
327
|
+
Interfaces may still change during the developer-release series.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
Ropemother is released under the MIT License. See `LICENSE` for details.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Ropemother
|
|
2
|
+
|
|
3
|
+
`ropemother` is a Python package for building small message-oriented systems. It provides publish-subscribe messaging, request/reply helpers, capture and history support, portable payload formats, an in-process direct broker, and a freestanding broker for communication between local processes.
|
|
4
|
+
|
|
5
|
+
The current developer release is intended for teaching, research software, local development, and early integration work.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
`ropemother` requires Python 3.13 or newer.
|
|
10
|
+
|
|
11
|
+
Install the current developer release from PyPI:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python -m pip install --pre ropemother
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The base package has no required third-party runtime dependencies. The exploratory ZeroMQ transport is available as an optional extra:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python -m pip install --pre "ropemother[zmq]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Publish and subscribe
|
|
24
|
+
|
|
25
|
+
The examples below introduce the main public messaging interfaces with small programs.
|
|
26
|
+
|
|
27
|
+
A direct message bus can route a message from one emitter to every receiver whose subscription matches that message.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
31
|
+
|
|
32
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
33
|
+
|
|
34
|
+
emitter = bus.register_emitter(
|
|
35
|
+
msg_topic="foo-topic.events",
|
|
36
|
+
msg_producer="producer-corge",
|
|
37
|
+
msg_type="type-garply",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
first_receiver = bus.subscribe(
|
|
41
|
+
msg_topic="foo-topic.events",
|
|
42
|
+
msg_producer="producer-corge",
|
|
43
|
+
msg_type="type-garply",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
second_receiver = bus.subscribe(
|
|
47
|
+
msg_topic="foo-topic.events",
|
|
48
|
+
msg_producer="producer-corge",
|
|
49
|
+
msg_type="type-garply",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
emitter.emit("hello from producer")
|
|
53
|
+
|
|
54
|
+
print(first_receiver.receive().payload)
|
|
55
|
+
print(second_receiver.receive().payload)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Expected output:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
hello from producer
|
|
62
|
+
hello from producer
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`msg_topic`, `msg_producer`, and `msg_type` describe the messages that an endpoint emits or receives. In the example, both receivers select messages produced by `producer-corge` on `foo-topic.events` with message type `type-garply`, so both receive the same broadcast message.
|
|
66
|
+
|
|
67
|
+
The direct broker is useful when the participating components can share one Python process. The same endpoint vocabulary is also used with the freestanding broker described below.
|
|
68
|
+
|
|
69
|
+
## Running a freestanding broker
|
|
70
|
+
|
|
71
|
+
A broker coordinates message delivery. The freestanding broker lets separate local processes connect to the same message bus.
|
|
72
|
+
|
|
73
|
+
Start a local broker in one terminal:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
python -m ropemother.service
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The broker prints its connection descriptor and an environment-variable form that client processes can use:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
Message bus broker is running
|
|
83
|
+
broker URI: ropemother+unix:///...
|
|
84
|
+
environment: ROPEMOTHER_CONNECTION_DESCRIPTOR=ropemother+unix:///...
|
|
85
|
+
Press Ctrl-C to stop
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The connection descriptor identifies the broker endpoint that client processes use to find the same message bus.
|
|
89
|
+
|
|
90
|
+
A client can connect using the printed descriptor explicitly:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from ropemother import connect_message_bus
|
|
94
|
+
|
|
95
|
+
bus = connect_message_bus("ropemother+unix:///...")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If `ROPEMOTHER_CONNECTION_DESCRIPTOR` is already set in the environment, the descriptor can be omitted:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from ropemother import connect_message_bus
|
|
102
|
+
|
|
103
|
+
bus = connect_message_bus()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Once connected, client code elsewhere uses the same `register_emitter(...)`, `subscribe(...)`, `emit(...)`, and `receive()` operations as a direct bus.
|
|
107
|
+
|
|
108
|
+
For example, a subscriber process can wait for one message:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from ropemother import connect_message_bus
|
|
112
|
+
|
|
113
|
+
bus = connect_message_bus()
|
|
114
|
+
receiver = bus.subscribe(
|
|
115
|
+
msg_topic="foo-topic.events",
|
|
116
|
+
msg_producer="producer-corge",
|
|
117
|
+
msg_type="type-garply",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
message = receiver.receive()
|
|
121
|
+
print(message.payload)
|
|
122
|
+
|
|
123
|
+
bus.close()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A separate producer process can publish the message:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from ropemother import connect_message_bus
|
|
130
|
+
|
|
131
|
+
bus = connect_message_bus()
|
|
132
|
+
emitter = bus.register_emitter(
|
|
133
|
+
msg_topic="foo-topic.events",
|
|
134
|
+
msg_producer="producer-corge",
|
|
135
|
+
msg_type="type-garply",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
emitter.emit("hello from producer")
|
|
139
|
+
bus.close()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The broker allows the producer, subscriber, and other services to have independent process lifetimes while preserving the same application-facing message model.
|
|
143
|
+
|
|
144
|
+
## Request and reply
|
|
145
|
+
|
|
146
|
+
A request/reply service places an application operation behind a message boundary. The client sends a request and waits for the correlated reply instead of calling the service implementation directly.
|
|
147
|
+
|
|
148
|
+
The following local example uses `str.upper` as the service operation:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import asyncio
|
|
152
|
+
|
|
153
|
+
from ropemother import AsyncDirectMessageBus, InMemoryCaptureSink
|
|
154
|
+
|
|
155
|
+
bus = AsyncDirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
156
|
+
|
|
157
|
+
client = bus.create_procedure_client(
|
|
158
|
+
request_topic="foo-topic.requests",
|
|
159
|
+
reply_topic="foo-topic.replies",
|
|
160
|
+
requester_producer="producer-corge",
|
|
161
|
+
responder_producer="producer-grault",
|
|
162
|
+
request_msg_type="type-garply",
|
|
163
|
+
reply_msg_type="type-waldo",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
service = bus.create_procedure_service(
|
|
167
|
+
request_topic="foo-topic.requests",
|
|
168
|
+
reply_topic="foo-topic.replies",
|
|
169
|
+
requester_producer="producer-corge",
|
|
170
|
+
responder_producer="producer-grault",
|
|
171
|
+
request_msg_type="type-garply",
|
|
172
|
+
reply_msg_type="type-waldo",
|
|
173
|
+
handler=str.upper,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def run_one_request() -> str:
|
|
178
|
+
service_task = asyncio.create_task(service.handle())
|
|
179
|
+
result = await client("hello")
|
|
180
|
+
await service_task
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
print(asyncio.run(run_one_request()))
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Expected output:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
HELLO
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A procedure client is callable and returns the reply payload. `client.call(...)` provides the same payload-returning operation with an explicit method name, while `client.call_reply(...)` returns the full reply message when application code also needs its message metadata.
|
|
194
|
+
|
|
195
|
+
## Capture and history
|
|
196
|
+
|
|
197
|
+
Capture records messages and the information needed to interpret them later.
|
|
198
|
+
|
|
199
|
+
Small in-process applications can supply a capture sink when constructing a direct bus:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
203
|
+
|
|
204
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The freestanding broker enables capture by default and writes JSON Lines records to `.ropemother/capture.jsonl`:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
python -m ropemother.service
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use `--capture-path` to choose another capture file. Use `--transport-only` only when a local transport experiment explicitly does not need capture, history, or replay guarantees.
|
|
214
|
+
|
|
215
|
+
History is the application-facing way to query captured messages. Start the broker with its built-in history service:
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
python -m ropemother.service --history
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Then create the preconfigured client for that service:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from ropemother import connect_message_bus
|
|
225
|
+
from ropemother.service import preconfigured_history_client
|
|
226
|
+
|
|
227
|
+
bus = connect_message_bus()
|
|
228
|
+
|
|
229
|
+
emitter = bus.register_emitter(
|
|
230
|
+
msg_topic="foo-topic.events",
|
|
231
|
+
msg_producer="producer-corge",
|
|
232
|
+
msg_type="type-plugh",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
history_client = preconfigured_history_client(bus)
|
|
236
|
+
|
|
237
|
+
emitter.emit("captured event")
|
|
238
|
+
|
|
239
|
+
page = history_client.select(
|
|
240
|
+
msg_topic="foo-topic.events",
|
|
241
|
+
msg_producer="producer-corge",
|
|
242
|
+
msg_type="type-plugh",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
print(page.entries[0].payload)
|
|
246
|
+
bus.close()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Expected output:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
captured event
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Application code queries the history service through the bus rather than opening the capture file directly. `preconfigured_history_client(...)` supplies the fixed contract used by the broker's built-in history service. Applications that define a different history service can use the explicit history client and service constructors instead.
|
|
256
|
+
|
|
257
|
+
## Portable payloads
|
|
258
|
+
|
|
259
|
+
Portable formats serialize payloads for capture and transport. `ropemother` includes JSON and raw-byte formats and supports project-defined formats for other application data.
|
|
260
|
+
|
|
261
|
+
## Using the source checkout
|
|
262
|
+
|
|
263
|
+
Clone the repository and install its development dependencies into an environment chosen for development work:
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
git clone https://github.com/edurange/ropemother.git
|
|
267
|
+
cd ropemother
|
|
268
|
+
python -m pip install -e ".[dev]"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The source repository is available at:
|
|
272
|
+
|
|
273
|
+
<https://github.com/edurange/ropemother>
|
|
274
|
+
|
|
275
|
+
Issues are tracked at:
|
|
276
|
+
|
|
277
|
+
<https://github.com/edurange/ropemother/issues>
|
|
278
|
+
|
|
279
|
+
See `CONTRIBUTING.md` before preparing a substantial change.
|
|
280
|
+
|
|
281
|
+
### Executable development demos
|
|
282
|
+
|
|
283
|
+
`ropemother/playground.py` contains chronological executable demonstrations used for development, inspection, and smoke checking:
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
python -m ropemother.playground
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The playground is intentionally more verbose than the short examples in this README and is mainly useful for development and inspection.
|
|
290
|
+
|
|
291
|
+
## Supplementary exercises
|
|
292
|
+
|
|
293
|
+
Exercises and tutorial materials are available at:
|
|
294
|
+
|
|
295
|
+
<https://github.com/edurange/ropemother-exercises>
|
|
296
|
+
|
|
297
|
+
## Development status
|
|
298
|
+
|
|
299
|
+
`ropemother` is a developer release. The current implementation covers the local message model, direct and freestanding broker operation, portable payload formats, capture, history queries, and request/reply helpers.
|
|
300
|
+
|
|
301
|
+
Interfaces may still change during the developer-release series.
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
Ropemother is released under the MIT License. See `LICENSE` for details.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ropemother"
|
|
7
|
-
version = "0.1.0.
|
|
7
|
+
version = "0.1.0.dev9"
|
|
8
8
|
description = "A module for publish-subscribe broadcast messaging."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -36,10 +36,11 @@ dev = [
|
|
|
36
36
|
[project.urls]
|
|
37
37
|
Source = "https://github.com/edurange/ropemother"
|
|
38
38
|
Issues = "https://github.com/edurange/ropemother/issues"
|
|
39
|
+
Exercises = "https://github.com/edurange/ropemother-exercises"
|
|
39
40
|
|
|
40
41
|
[tool.black]
|
|
41
42
|
line-length = 79
|
|
42
43
|
target-version = ["py313"]
|
|
43
44
|
|
|
44
45
|
[tool.pylint.main]
|
|
45
|
-
py-version = "3.13"
|
|
46
|
+
py-version = "3.13"
|