ropemother 0.1.0.dev8__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.dev8/LICENSE +9 -0
- ropemother-0.1.0.dev8/PKG-INFO +462 -0
- ropemother-0.1.0.dev8/README.md +437 -0
- ropemother-0.1.0.dev8/pyproject.toml +45 -0
- ropemother-0.1.0.dev8/ropemother/__init__.py +46 -0
- ropemother-0.1.0.dev8/ropemother/bootstrap/__init__.py +11 -0
- ropemother-0.1.0.dev8/ropemother/bootstrap/buffer.py +127 -0
- ropemother-0.1.0.dev8/ropemother/bootstrap/policy.py +105 -0
- ropemother-0.1.0.dev8/ropemother/broker/__init__.py +37 -0
- ropemother-0.1.0.dev8/ropemother/broker/asyncbase.py +58 -0
- ropemother-0.1.0.dev8/ropemother/broker/asyncdirect.py +349 -0
- ropemother-0.1.0.dev8/ropemother/broker/asyncendpoints.py +106 -0
- ropemother-0.1.0.dev8/ropemother/broker/base.py +59 -0
- ropemother-0.1.0.dev8/ropemother/broker/direct.py +338 -0
- ropemother-0.1.0.dev8/ropemother/broker/directcore.py +863 -0
- ropemother-0.1.0.dev8/ropemother/broker/dispatch.py +66 -0
- ropemother-0.1.0.dev8/ropemother/broker/endpoints.py +152 -0
- ropemother-0.1.0.dev8/ropemother/broker/subscription.py +34 -0
- ropemother-0.1.0.dev8/ropemother/capture/__init__.py +52 -0
- ropemother-0.1.0.dev8/ropemother/capture/controller.py +207 -0
- ropemother-0.1.0.dev8/ropemother/capture/filehistory.py +239 -0
- ropemother-0.1.0.dev8/ropemother/capture/filesink.py +72 -0
- ropemother-0.1.0.dev8/ropemother/capture/history.py +405 -0
- ropemother-0.1.0.dev8/ropemother/capture/historyselection.py +69 -0
- ropemother-0.1.0.dev8/ropemother/capture/historyservice.py +763 -0
- ropemother-0.1.0.dev8/ropemother/capture/jsonrecords.py +251 -0
- ropemother-0.1.0.dev8/ropemother/capture/memorysink.py +64 -0
- ropemother-0.1.0.dev8/ropemother/capture/runtime.py +33 -0
- ropemother-0.1.0.dev8/ropemother/capture/sink.py +64 -0
- ropemother-0.1.0.dev8/ropemother/capture/writer.py +83 -0
- ropemother-0.1.0.dev8/ropemother/client/__init__.py +58 -0
- ropemother-0.1.0.dev8/ropemother/client/asyncendpointfactory.py +146 -0
- ropemother-0.1.0.dev8/ropemother/client/asyncendpointprovisioner.py +383 -0
- ropemother-0.1.0.dev8/ropemother/client/asyncrequest.py +405 -0
- ropemother-0.1.0.dev8/ropemother/client/endpointfactory.py +141 -0
- ropemother-0.1.0.dev8/ropemother/client/endpointfactorybase.py +378 -0
- ropemother-0.1.0.dev8/ropemother/client/lifecycle.py +75 -0
- ropemother-0.1.0.dev8/ropemother/client/procedure.py +151 -0
- ropemother-0.1.0.dev8/ropemother/client/request.py +568 -0
- ropemother-0.1.0.dev8/ropemother/client/requestoptions.py +23 -0
- ropemother-0.1.0.dev8/ropemother/exceptions.py +61 -0
- ropemother-0.1.0.dev8/ropemother/fixtures/__init__.py +30 -0
- ropemother-0.1.0.dev8/ropemother/fixtures/scriptedinput.py +437 -0
- ropemother-0.1.0.dev8/ropemother/format/__init__.py +60 -0
- ropemother-0.1.0.dev8/ropemother/format/defaults.py +42 -0
- ropemother-0.1.0.dev8/ropemother/format/formattable.py +39 -0
- ropemother-0.1.0.dev8/ropemother/format/portableformat.py +191 -0
- ropemother-0.1.0.dev8/ropemother/format/registry.py +201 -0
- ropemother-0.1.0.dev8/ropemother/message/__init__.py +37 -0
- ropemother-0.1.0.dev8/ropemother/message/messageidentity.py +23 -0
- ropemother-0.1.0.dev8/ropemother/message/records.py +121 -0
- ropemother-0.1.0.dev8/ropemother/message/registrationtable.py +185 -0
- ropemother-0.1.0.dev8/ropemother/message/selectors.py +191 -0
- ropemother-0.1.0.dev8/ropemother/message/symbols.py +241 -0
- ropemother-0.1.0.dev8/ropemother/message/typeformats.py +239 -0
- ropemother-0.1.0.dev8/ropemother/playground.py +7841 -0
- ropemother-0.1.0.dev8/ropemother/service/__init__.py +61 -0
- ropemother-0.1.0.dev8/ropemother/service/__main__.py +19 -0
- ropemother-0.1.0.dev8/ropemother/service/asyncservice.py +184 -0
- ropemother-0.1.0.dev8/ropemother/service/broker.py +278 -0
- ropemother-0.1.0.dev8/ropemother/service/brokerextension.py +31 -0
- ropemother-0.1.0.dev8/ropemother/service/brokerhistory.py +78 -0
- ropemother-0.1.0.dev8/ropemother/service/connector.py +91 -0
- ropemother-0.1.0.dev8/ropemother/service/descriptor.py +104 -0
- ropemother-0.1.0.dev8/ropemother/service/environment.py +161 -0
- ropemother-0.1.0.dev8/ropemother/service/host.py +272 -0
- ropemother-0.1.0.dev8/ropemother/service/listener.py +47 -0
- ropemother-0.1.0.dev8/ropemother/service/resource.py +58 -0
- ropemother-0.1.0.dev8/ropemother/service/service.py +200 -0
- ropemother-0.1.0.dev8/ropemother/service/socketlistener.py +280 -0
- ropemother-0.1.0.dev8/ropemother/transport/__init__.py +46 -0
- ropemother-0.1.0.dev8/ropemother/transport/asyncclient.py +694 -0
- ropemother-0.1.0.dev8/ropemother/transport/asyncconnection.py +101 -0
- ropemother-0.1.0.dev8/ropemother/transport/asyncsession.py +304 -0
- ropemother-0.1.0.dev8/ropemother/transport/asyncsessionrunner.py +108 -0
- ropemother-0.1.0.dev8/ropemother/transport/asyncsocketconnection.py +102 -0
- ropemother-0.1.0.dev8/ropemother/transport/client.py +679 -0
- ropemother-0.1.0.dev8/ropemother/transport/codec.py +743 -0
- ropemother-0.1.0.dev8/ropemother/transport/connection.py +105 -0
- ropemother-0.1.0.dev8/ropemother/transport/endpointregistration.py +86 -0
- ropemother-0.1.0.dev8/ropemother/transport/frames.py +150 -0
- ropemother-0.1.0.dev8/ropemother/transport/session.py +293 -0
- ropemother-0.1.0.dev8/ropemother/transport/sessionrunner.py +107 -0
- ropemother-0.1.0.dev8/ropemother/transport/sessionstate.py +119 -0
- ropemother-0.1.0.dev8/ropemother/transport/socketconnection.py +81 -0
- ropemother-0.1.0.dev8/ropemother/transport/socketframing.py +98 -0
- ropemother-0.1.0.dev8/ropemother/transport/zeromq/__init__.py +11 -0
- ropemother-0.1.0.dev8/ropemother/transport/zeromq/address.py +29 -0
- ropemother-0.1.0.dev8/ropemother/transport/zeromq/connection.py +169 -0
- ropemother-0.1.0.dev8/ropemother/util/__init__.py +57 -0
- ropemother-0.1.0.dev8/ropemother/util/compositeblobserializer.py +110 -0
- ropemother-0.1.0.dev8/ropemother/util/lengthprefixed.py +112 -0
- ropemother-0.1.0.dev8/ropemother/util/onelinejson.py +62 -0
- ropemother-0.1.0.dev8/ropemother/util/serializer.py +101 -0
- ropemother-0.1.0.dev8/ropemother/util/symbol.py +34 -0
- ropemother-0.1.0.dev8/ropemother/util/typedid.py +66 -0
- ropemother-0.1.0.dev8/ropemother.egg-info/PKG-INFO +462 -0
- ropemother-0.1.0.dev8/ropemother.egg-info/SOURCES.txt +100 -0
- ropemother-0.1.0.dev8/ropemother.egg-info/dependency_links.txt +1 -0
- ropemother-0.1.0.dev8/ropemother.egg-info/requires.txt +13 -0
- ropemother-0.1.0.dev8/ropemother.egg-info/top_level.txt +1 -0
- ropemother-0.1.0.dev8/setup.cfg +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Released under MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joe Granville and eduRange contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ropemother
|
|
3
|
+
Version: 0.1.0.dev8
|
|
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
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Provides-Extra: zmq
|
|
15
|
+
Requires-Dist: pyzmq; extra == "zmq"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: black; extra == "dev"
|
|
20
|
+
Requires-Dist: build; extra == "dev"
|
|
21
|
+
Requires-Dist: pylint; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: pyzmq; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# ropemother
|
|
27
|
+
|
|
28
|
+
This is a developer release of `ropemother`, a Python package for building small message-oriented systems. It provides a low-configuration direct broker, readable topic/type/producer names, capture support, portable payload formats, and request/reply helpers for simple local services.
|
|
29
|
+
|
|
30
|
+
The direct broker is useful for local development, teaching, demos, and early integration work. It is not intended to be the final transport story for every deployment. The package is organized so code can begin against project-owned message concepts while leaving room for later transport adapters and stronger persistence infrastructure.
|
|
31
|
+
|
|
32
|
+
This preview does not claim to provide distributed consensus, production broker deployment, complete replay orchestration, or archive-level storage integration. It focuses on the local message model, capture behavior, request/reply helpers, and readable examples that can be used while the broader service architecture is still taking shape.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
`ropemother` requires Python 3.13 or newer.
|
|
37
|
+
|
|
38
|
+
Install the current developer release from PyPI with:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python -m pip install --pre ropemother
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The base package has no required third-party runtime dependencies. The exploratory ZeroMQ transport is available as an optional extra:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
python -m pip install --pre "ropemother[zmq]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Using the source checkout
|
|
51
|
+
|
|
52
|
+
Clone the repository into a workspace directory:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
git clone https://github.com/edurange/ropemother.git
|
|
56
|
+
cd ropemother
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Install the development dependencies into the current environment:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
python -m pip install -e ".[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run preview commands from the repository root:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
python -m ropemother.playground
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use the same working-directory setup for small scripts kept in the checkout:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
python my_example.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If an example also imports a companion source tree such as `intarsia`, keep the repositories next to each other and add the companion tree to `PYTHONPATH`:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
workspace/
|
|
81
|
+
ropemother/
|
|
82
|
+
intarsia/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
PYTHONPATH="$PWD:../intarsia" python my_example.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The freestanding broker uses the same source-checkout setup. Its startup and client commands are shown later in this README.
|
|
90
|
+
|
|
91
|
+
## Publish and subscribe
|
|
92
|
+
|
|
93
|
+
The direct broker routes a message from an emitter to every receiver whose subscription matches that message.
|
|
94
|
+
|
|
95
|
+
This example keeps the emitter and receivers in one Python session so you can run it locally. In a larger application, the producer and subscribers may belong to different components.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
99
|
+
|
|
100
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
101
|
+
|
|
102
|
+
emitter = bus.register_emitter(
|
|
103
|
+
msg_topic="foo-topic.events",
|
|
104
|
+
msg_producer="producer-corge",
|
|
105
|
+
msg_type="type-garply",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
first_receiver = bus.subscribe(
|
|
109
|
+
msg_topic="foo-topic.events",
|
|
110
|
+
msg_producer="producer-corge",
|
|
111
|
+
msg_type="type-garply",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
second_receiver = bus.subscribe(
|
|
115
|
+
msg_topic="foo-topic.events",
|
|
116
|
+
msg_producer="producer-corge",
|
|
117
|
+
msg_type="type-garply",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
canonical_payload = "hello from producer"
|
|
121
|
+
|
|
122
|
+
emitter.emit(canonical_payload)
|
|
123
|
+
|
|
124
|
+
first_message = first_receiver.receive()
|
|
125
|
+
second_message = second_receiver.receive()
|
|
126
|
+
|
|
127
|
+
print(first_message.payload)
|
|
128
|
+
print(second_message.payload)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Expected output:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
hello from producer
|
|
135
|
+
hello from producer
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`producer-corge` identifies the component that produced the message. It does not identify either receiver.
|
|
139
|
+
|
|
140
|
+
Both receivers subscribed to messages from `producer-corge` on `foo-topic.events` with message type `type-garply`, so both receive the emitted payload.
|
|
141
|
+
|
|
142
|
+
## A small request/reply service
|
|
143
|
+
|
|
144
|
+
A service can be ordinary application code behind a message boundary. In this example, the service receives a string request, applies Python’s built-in `str.upper`, and sends the result back to the client.
|
|
145
|
+
|
|
146
|
+
`ropemother` provides the request/reply structure: the client sends a request message, the service handles it, and the client receives the reply. `str.upper` is a stand-in here for some application functionality hosted at a messaging endpoint, possibly a non-local one.
|
|
147
|
+
|
|
148
|
+
This is the first async example. The async code is here because the service must be waiting for a request while the client sends one and waits for the reply.
|
|
149
|
+
|
|
150
|
+
This example puts the client and service in the same Python session so you can run both sides locally. In a larger application, the client side and service side would usually belong to different components. The important point is that the client does not call `str.upper` directly; it sends a request message across a message boundary.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
|
|
155
|
+
from ropemother import AsyncDirectMessageBus, InMemoryCaptureSink
|
|
156
|
+
|
|
157
|
+
bus = AsyncDirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
158
|
+
|
|
159
|
+
# Client-side request endpoint
|
|
160
|
+
client = bus.create_procedure_client(
|
|
161
|
+
request_topic="foo-topic.requests",
|
|
162
|
+
reply_topic="foo-topic.replies",
|
|
163
|
+
requester_producer="producer-corge",
|
|
164
|
+
responder_producer="producer-grault",
|
|
165
|
+
request_msg_type="type-garply",
|
|
166
|
+
reply_msg_type="type-waldo",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Service-side request handler
|
|
170
|
+
service = bus.create_procedure_service(
|
|
171
|
+
request_topic="foo-topic.requests",
|
|
172
|
+
reply_topic="foo-topic.replies",
|
|
173
|
+
requester_producer="producer-corge",
|
|
174
|
+
responder_producer="producer-grault",
|
|
175
|
+
request_msg_type="type-garply",
|
|
176
|
+
reply_msg_type="type-waldo",
|
|
177
|
+
handler=str.upper,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def run_one_request_reply_exchange() -> str:
|
|
182
|
+
service_task = asyncio.create_task(service.handle())
|
|
183
|
+
|
|
184
|
+
received_payload = await client("hello")
|
|
185
|
+
|
|
186
|
+
await service_task
|
|
187
|
+
|
|
188
|
+
return received_payload
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
received_payload = asyncio.run(run_one_request_reply_exchange())
|
|
192
|
+
print(received_payload)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Expected output:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
HELLO
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`foo-topic.requests` is where the service receives requests. `foo-topic.replies` is where the client receives replies. `producer-corge` identifies the requesting component, and `producer-grault` identifies the responding component. `type-garply` identifies the request message type, and `type-waldo` identifies the reply message type.
|
|
202
|
+
|
|
203
|
+
`run_one_request_reply_exchange()` exists because the service handler and client call need to share one event loop. The service task waits for one request while the client sends `"hello"` and waits for the reply payload. The final `await service_task` lets the one-request service handler finish before the example exits.
|
|
204
|
+
|
|
205
|
+
A procedure client is callable. Calling the client uses ordinary Python function arguments and returns the reply payload. `call(...)` provides the same payload-returning operation with an explicit method name. `call_reply(...)` returns the full reply message rather than just the procedure result, which should only be necessary for advanced applications.
|
|
206
|
+
|
|
207
|
+
In the local example, both sides are visible. In application code, these responsibilities often separate.
|
|
208
|
+
|
|
209
|
+
The service side owns the handler:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
service = bus.create_procedure_service(
|
|
213
|
+
request_topic="foo-topic.requests",
|
|
214
|
+
reply_topic="foo-topic.replies",
|
|
215
|
+
requester_producer="producer-corge",
|
|
216
|
+
responder_producer="producer-grault",
|
|
217
|
+
request_msg_type="type-garply",
|
|
218
|
+
reply_msg_type="type-waldo",
|
|
219
|
+
handler=str.upper,
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The client side owns the request:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
client = bus.create_procedure_client(
|
|
227
|
+
request_topic="foo-topic.requests",
|
|
228
|
+
reply_topic="foo-topic.replies",
|
|
229
|
+
requester_producer="producer-corge",
|
|
230
|
+
responder_producer="producer-grault",
|
|
231
|
+
request_msg_type="type-garply",
|
|
232
|
+
reply_msg_type="type-waldo",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
received = await client("hello")
|
|
236
|
+
|
|
237
|
+
# Use call_reply(...) when application code needs the reply message metadata.
|
|
238
|
+
reply = await client.call_reply("hello")
|
|
239
|
+
received_again = reply.payload
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Running a freestanding broker
|
|
243
|
+
|
|
244
|
+
The earlier examples create an in-process direct broker:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from ropemother import DirectMessageBus, InMemoryCaptureSink
|
|
248
|
+
|
|
249
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For application-scale work, it will often be useful to run the bus as a freestanding broker and connect client processes to it.
|
|
253
|
+
|
|
254
|
+
Start the broker in one terminal:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
python -m ropemother.service
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The broker prints both the explicit broker URI and an environment-variable form:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
Message bus broker is running
|
|
264
|
+
broker URI: ropemother+unix:///...
|
|
265
|
+
environment: ROPEMOTHER_CONNECTION_DESCRIPTOR=ropemother+unix:///...
|
|
266
|
+
Press Ctrl-C to stop
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
For teaching examples, copy the printed broker URI directly into the client code. Each client process uses that URI to find the broker.
|
|
270
|
+
|
|
271
|
+
### Subscriber process
|
|
272
|
+
|
|
273
|
+
Save this as `subscriber.py`, then run it in a second terminal. It waits for one message.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from ropemother import connect_message_bus
|
|
277
|
+
|
|
278
|
+
broker_uri = "ropemother+unix:///..."
|
|
279
|
+
bus = connect_message_bus(broker_uri)
|
|
280
|
+
|
|
281
|
+
receiver = bus.subscribe(
|
|
282
|
+
msg_topic="foo-topic.events",
|
|
283
|
+
msg_producer="producer-corge",
|
|
284
|
+
msg_type="type-garply",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
message = receiver.receive()
|
|
288
|
+
print(message.payload)
|
|
289
|
+
|
|
290
|
+
bus.close()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Run it while the broker is still running:
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
python subscriber.py
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Producer process
|
|
300
|
+
|
|
301
|
+
Save this as `producer.py`, then run it in a third terminal.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from ropemother import connect_message_bus
|
|
305
|
+
|
|
306
|
+
broker_uri = "ropemother+unix:///..."
|
|
307
|
+
bus = connect_message_bus(broker_uri)
|
|
308
|
+
|
|
309
|
+
emitter = bus.register_emitter(
|
|
310
|
+
msg_topic="foo-topic.events",
|
|
311
|
+
msg_producer="producer-corge",
|
|
312
|
+
msg_type="type-garply",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
emitter.emit("hello from producer")
|
|
316
|
+
|
|
317
|
+
bus.close()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Run it with the same broker URI in the script:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
python producer.py
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
As a deployment or convenience option, a process may also read the broker URI from the environment variable `ROPEMOTHER_CONNECTION_DESCRIPTOR` when no URI is provided:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from ropemother import connect_message_bus
|
|
330
|
+
|
|
331
|
+
bus = connect_message_bus()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Use that form when the environment is responsible for providing the connection descriptor.
|
|
335
|
+
|
|
336
|
+
The subscriber prints:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
hello from producer
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
With `DirectMessageBus`, the broker object is created inside the Python process. With `connect_message_bus()`, the Python process connects to the freestanding broker. The endpoint vocabulary stays the same: register an emitter, subscribe a receiver, emit a message, and receive a message.
|
|
343
|
+
|
|
344
|
+
## Capture, logging, and history
|
|
345
|
+
|
|
346
|
+
Capture records the symbol registrations and messages that pass through the broker. It is the normal posture for `ropemother`, because later inspection, replay-oriented tools, and history queries only make sense when the run has preserved an interpretable message log.
|
|
347
|
+
|
|
348
|
+
For small local examples, the capture sink can be attached when the bus is constructed:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
bus = DirectMessageBus(capture_sink=InMemoryCaptureSink())
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Managed applications may create the bus before the final capture sink is ready:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
bus = DirectMessageBus()
|
|
358
|
+
|
|
359
|
+
# Startup and registration work may happen here.
|
|
360
|
+
|
|
361
|
+
bus.set_capture_sink(capture_sink)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
This second form is intended for service-style startup, where the bus and its capture sink may have different readiness lifecycles. While capture is enabled but no sink is attached, the bus may perform limited bootstrap work, but ordinary emitted messages are rejected rather than delivered without capture.
|
|
365
|
+
|
|
366
|
+
For the freestanding broker, the default command starts a local broker with capture enabled and writes captured records to `.ropemother/capture.jsonl`:
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
python -m ropemother.service
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Use `--history` to start the broker's built-in history service, so application code can query captured message history:
|
|
373
|
+
|
|
374
|
+
```sh
|
|
375
|
+
python -m ropemother.service --history
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Use `--capture-path` to choose a different JSON Lines capture file.
|
|
379
|
+
|
|
380
|
+
Use `--transport-only` only when you explicitly want a no-capture broker for local transport experiments. Transport-only mode routes messages without capture, history, or replay guarantees.
|
|
381
|
+
|
|
382
|
+
History is the application-facing way to ask about prior messages. Most application code should not parse the capture JSON Lines file directly.
|
|
383
|
+
|
|
384
|
+
Captured payloads are intended to be portable. A value that can be handed across an in-process receiver queue is not automatically a good persistent or cross-runtime payload. The preview includes basic JSON and raw-byte formats for adoption, prototyping, capture, and public-boundary interoperability; projects can add dedicated formats for their own message families later. Stable project interfaces should usually prefer narrow message contracts over treating generic JSON structures as the internal data model.
|
|
385
|
+
|
|
386
|
+
## Query broker history from application code
|
|
387
|
+
|
|
388
|
+
Start a freestanding broker and enable its built-in history service:
|
|
389
|
+
|
|
390
|
+
```sh
|
|
391
|
+
python -m ropemother.service --history
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The broker prints a URI. Copy that URI into a client script.
|
|
395
|
+
|
|
396
|
+
Save this as `history_query.py`:
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
from ropemother import connect_message_bus
|
|
400
|
+
from ropemother.service import preconfigured_history_client
|
|
401
|
+
|
|
402
|
+
broker_uri = "ropemother+unix:///..."
|
|
403
|
+
bus = connect_message_bus(broker_uri)
|
|
404
|
+
|
|
405
|
+
emitter = bus.register_emitter(
|
|
406
|
+
msg_topic="foo-topic.events",
|
|
407
|
+
msg_producer="producer-corge",
|
|
408
|
+
msg_type="type-plugh",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
history_client = preconfigured_history_client(bus)
|
|
412
|
+
|
|
413
|
+
emitter.emit("captured event")
|
|
414
|
+
|
|
415
|
+
page = history_client.select(
|
|
416
|
+
msg_topic="foo-topic.events",
|
|
417
|
+
msg_producer="producer-corge",
|
|
418
|
+
msg_type="type-plugh",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
print(page.entries[0].payload)
|
|
422
|
+
|
|
423
|
+
bus.close()
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Run it with the broker's history service running:
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
python history_query.py
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Expected output:
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
captured event
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The application code does not open the capture file and does not create the history service. It connects to the broker and sends a history request through the broker's built-in history service contract.
|
|
439
|
+
|
|
440
|
+
`preconfigured_history_client(...)` is the local/default wiring helper for that built-in broker history profile. It supplies the fixed request topic, reply topic, producer names, message types, and payload formats used by `python -m ropemother.service --history`.
|
|
441
|
+
|
|
442
|
+
That helper does not create history by magic, and it is not the general custom-service API. It is a shortcut over `create_history_client(...)` for this one preconfigured service contract. Code that uses a custom history service should call `create_history_client(...)` directly with explicit topics, producers, message types, and payload formats. A custom service should be constructed on the service side with matching `create_history_service(...)` parameters.
|
|
443
|
+
|
|
444
|
+
## Executable demos
|
|
445
|
+
|
|
446
|
+
The `playground.py` file contains executable demonstrations of more `ropemother` behavior.
|
|
447
|
+
|
|
448
|
+
The playground is intentionally more verbose than the README examples. It prints intermediate values, compares sent and received messages, and shows several feature combinations in one place. Use it when you want to see working examples beyond the short copy-paste sections above.
|
|
449
|
+
|
|
450
|
+
The playground is a preview-era teaching and validation file. Over time, smaller examples and formal tests should replace some of its responsibilities.
|
|
451
|
+
|
|
452
|
+
## Preview status
|
|
453
|
+
|
|
454
|
+
`ropemother` is a preview package. The current iteration focuses on the local message model, direct broker behavior, capture, request/reply helpers, history queries, and a freestanding broker process for local development.
|
|
455
|
+
|
|
456
|
+
The preview does not claim to provide a production distributed broker, consensus ordering, complete replay orchestration, or final transport infrastructure.
|
|
457
|
+
|
|
458
|
+
The direct broker is useful for local development and early integration work. Code that uses `register_emitter(...)`, `subscribe(...)`, `emit(...)`, `receive()`, request/reply clients, and history clients should remain close to the intended public workflow as stronger transport and persistence pieces are added.
|
|
459
|
+
|
|
460
|
+
## License
|
|
461
|
+
|
|
462
|
+
`ropemother` is released under the MIT License. See LICENSE for details.
|