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.
Files changed (105) hide show
  1. ropemother-0.1.0.dev9/PKG-INFO +331 -0
  2. ropemother-0.1.0.dev9/README.md +305 -0
  3. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/pyproject.toml +3 -2
  4. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/__init__.py +1 -1
  5. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/__init__.py +1 -1
  6. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/buffer.py +1 -1
  7. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/bootstrap/policy.py +1 -1
  8. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/__init__.py +1 -1
  9. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncbase.py +1 -1
  10. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncdirect.py +1 -1
  11. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/asyncendpoints.py +1 -1
  12. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/base.py +1 -1
  13. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/direct.py +1 -1
  14. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/directcore.py +1 -1
  15. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/dispatch.py +1 -1
  16. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/endpoints.py +1 -1
  17. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/broker/subscription.py +1 -1
  18. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/__init__.py +1 -1
  19. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/controller.py +1 -1
  20. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/filehistory.py +1 -1
  21. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/filesink.py +1 -1
  22. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/history.py +1 -1
  23. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/historyselection.py +1 -1
  24. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/historyservice.py +1 -1
  25. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/jsonrecords.py +1 -1
  26. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/memorysink.py +1 -1
  27. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/runtime.py +1 -1
  28. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/sink.py +1 -1
  29. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/capture/writer.py +1 -1
  30. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/__init__.py +1 -1
  31. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncendpointfactory.py +1 -1
  32. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncendpointprovisioner.py +1 -1
  33. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/asyncrequest.py +1 -1
  34. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/endpointfactory.py +1 -1
  35. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/endpointfactorybase.py +1 -1
  36. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/lifecycle.py +1 -1
  37. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/procedure.py +1 -1
  38. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/request.py +1 -1
  39. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/client/requestoptions.py +1 -1
  40. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/exceptions.py +1 -1
  41. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/fixtures/__init__.py +1 -1
  42. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/fixtures/scriptedinput.py +1 -1
  43. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/__init__.py +1 -1
  44. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/defaults.py +1 -1
  45. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/formattable.py +1 -1
  46. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/portableformat.py +1 -1
  47. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/format/registry.py +1 -1
  48. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/__init__.py +1 -1
  49. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/messageidentity.py +1 -1
  50. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/records.py +1 -1
  51. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/registrationtable.py +1 -1
  52. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/selectors.py +1 -1
  53. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/symbols.py +1 -1
  54. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/message/typeformats.py +1 -1
  55. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/playground.py +1 -1
  56. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/__init__.py +1 -1
  57. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/__main__.py +1 -1
  58. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/asyncservice.py +1 -1
  59. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/broker.py +1 -1
  60. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/brokerextension.py +1 -1
  61. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/brokerhistory.py +1 -1
  62. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/connector.py +1 -1
  63. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/descriptor.py +1 -1
  64. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/environment.py +1 -1
  65. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/host.py +1 -1
  66. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/listener.py +1 -1
  67. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/resource.py +1 -1
  68. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/service.py +1 -1
  69. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/service/socketlistener.py +1 -1
  70. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/__init__.py +1 -1
  71. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncclient.py +1 -1
  72. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncconnection.py +1 -1
  73. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsession.py +1 -1
  74. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsessionrunner.py +1 -1
  75. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/asyncsocketconnection.py +1 -1
  76. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/client.py +1 -1
  77. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/codec.py +1 -1
  78. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/connection.py +1 -1
  79. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/endpointregistration.py +1 -1
  80. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/frames.py +1 -1
  81. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/session.py +1 -1
  82. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/sessionrunner.py +1 -1
  83. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/sessionstate.py +1 -1
  84. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/socketconnection.py +1 -1
  85. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/socketframing.py +1 -1
  86. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/__init__.py +1 -1
  87. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/address.py +1 -1
  88. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/transport/zeromq/connection.py +1 -1
  89. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/__init__.py +1 -1
  90. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/compositeblobserializer.py +1 -1
  91. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/lengthprefixed.py +1 -1
  92. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/onelinejson.py +1 -1
  93. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/serializer.py +1 -1
  94. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/symbol.py +1 -1
  95. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother/util/typedid.py +1 -1
  96. ropemother-0.1.0.dev9/ropemother.egg-info/PKG-INFO +331 -0
  97. ropemother-0.1.0.dev8/PKG-INFO +0 -462
  98. ropemother-0.1.0.dev8/README.md +0 -437
  99. ropemother-0.1.0.dev8/ropemother.egg-info/PKG-INFO +0 -462
  100. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/LICENSE +0 -0
  101. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/SOURCES.txt +0 -0
  102. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/dependency_links.txt +0 -0
  103. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/requires.txt +0 -0
  104. {ropemother-0.1.0.dev8 → ropemother-0.1.0.dev9}/ropemother.egg-info/top_level.txt +0 -0
  105. {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.dev8"
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"
@@ -10,7 +10,7 @@ __author__ = "Joe Granville"
10
10
  __email__ = "874605+jwgranville@users.noreply.github.com"
11
11
  __date__ = "2026-07-05T15:40:19+00:00"
12
12
  __license__ = "MIT"
13
- __version__ = "0.1.0.dev8"
13
+ __version__ = "0.1.0.dev9"
14
14
  __status__ = "Development"
15
15
 
16
16
 
@@ -7,5 +7,5 @@ __author__ = "Joe Granville"
7
7
  __email__ = "874605+jwgranville@users.noreply.github.com"
8
8
  __date__ = "2026-07-01T20:13:04+00:00"
9
9
  __license__ = "MIT"
10
- __version__ = "0.1.0.dev8"
10
+ __version__ = "0.1.0.dev9"
11
11
  __status__ = "Development"
@@ -16,7 +16,7 @@ __author__ = "Joe Granville"
16
16
  __email__ = "874605+jwgranville@users.noreply.github.com"
17
17
  __date__ = "2026-07-02T07:00:19+00:00"
18
18
  __license__ = "MIT"
19
- __version__ = "0.1.0.dev8"
19
+ __version__ = "0.1.0.dev9"
20
20
  __status__ = "Development"
21
21
 
22
22
 
@@ -14,7 +14,7 @@ __author__ = "Joe Granville"
14
14
  __email__ = "874605+jwgranville@users.noreply.github.com"
15
15
  __date__ = "2026-07-02T07:03:33+00:00"
16
16
  __license__ = "MIT"
17
- __version__ = "0.1.0.dev8"
17
+ __version__ = "0.1.0.dev9"
18
18
  __status__ = "Development"
19
19
 
20
20
 
@@ -10,7 +10,7 @@ __author__ = "Joe Granville"
10
10
  __email__ = "874605+jwgranville@users.noreply.github.com"
11
11
  __date__ = "2026-06-30T18:59:10+00:00"
12
12
  __license__ = "MIT"
13
- __version__ = "0.1.0.dev8"
13
+ __version__ = "0.1.0.dev9"
14
14
  __status__ = "Development"
15
15
 
16
16