formix-pubsub 0.9.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,405 @@
1
+ Metadata-Version: 2.4
2
+ Name: formix-pubsub
3
+ Version: 0.9.0
4
+ Summary: A Python publish-subscribe messaging library
5
+ Author-email: formix <formix@users.noreply.github.com>
6
+ Maintainer-email: formix <formix@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/formix/pubsub
9
+ Project-URL: Bug Tracker, https://github.com/formix/pubsub/issues
10
+ Project-URL: Documentation, https://github.com/formix/pubsub
11
+ Project-URL: Source Code, https://github.com/formix/pubsub
12
+ Keywords: pubsub,messaging,publish,subscribe,events
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Communications
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-cov; extra == "dev"
29
+ Requires-Dist: black; extra == "dev"
30
+ Requires-Dist: flake8; extra == "dev"
31
+ Requires-Dist: mypy; extra == "dev"
32
+ Requires-Dist: pre-commit; extra == "dev"
33
+ Provides-Extra: docs
34
+ Requires-Dist: sphinx; extra == "docs"
35
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
36
+ Dynamic: license-file
37
+
38
+ # PubSub
39
+
40
+ A lightweight, file-system based publish-subscribe messaging system for Python using FIFO (named pipes).
41
+
42
+ The version 0.9.0 was tested on linux thouroughly, need more testing on Windows, MacOs and BSD.
43
+
44
+ ## Features
45
+
46
+ - **Topic-based routing** with wildcard support (`=` for single word, `+` for multiple words)
47
+ - **Multiple subscribers** can listen to the same topic independently
48
+ - **Message persistence** via file system until consumed
49
+ - **Non-blocking operations** using FIFO queues
50
+ - **Context manager support** for automatic resource cleanup
51
+ - **Thread-safe** message publishing and consumption
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install formix-pubsub
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### Basic Publish-Subscribe
62
+
63
+ ```python
64
+ from pubsub import Channel, publish, subscribe
65
+
66
+ # Create a channel for a topic
67
+ channel = Channel(topic="news.sports")
68
+
69
+ # Use context manager to ensure proper cleanup
70
+ with channel:
71
+ # Publish a message
72
+ count = publish("news.sports", b"Team wins championship!")
73
+ print(f"Published to {count} channel(s)")
74
+
75
+ # Subscribe with a callback
76
+ def handle_message(message):
77
+ print(f"Received: {message.content.decode()}")
78
+
79
+ subscribe(channel, handle_message, timeout_seconds=5.0)
80
+ ```
81
+
82
+ ### Fetching Messages Manually
83
+
84
+ ```python
85
+ from pubsub import Channel, publish, fetch
86
+
87
+ channel = Channel(topic="alerts")
88
+
89
+ with channel:
90
+ # Publish some messages
91
+ publish("alerts", b"System starting")
92
+ publish("alerts", b"All systems operational")
93
+
94
+ # Fetch messages one at a time
95
+ message = fetch(channel)
96
+ while message:
97
+ print(f"{message.topic}: {message.content.decode()}")
98
+ message = fetch(channel)
99
+ ```
100
+
101
+ ### Wildcard Topics
102
+
103
+ ```python
104
+ from pubsub import Channel, subscribe
105
+
106
+ # Single word wildcard (=) - matches one word
107
+ channel = Channel(topic="news.=") # Matches: news.sports, news.tech, news.world
108
+
109
+ # Multiple word wildcard (+) - matches one or more words
110
+ channel = Channel(topic="logs.+") # Matches: logs.error, logs.app.debug, logs.system.critical
111
+
112
+ with channel:
113
+ # This channel will receive all matching messages
114
+ def handle_message(msg):
115
+ print(f"[{msg.topic}] {msg.content.decode()}")
116
+
117
+ subscribe(channel, handle_message, timeout_seconds=10.0)
118
+ ```
119
+
120
+ ### Multiple Subscribers
121
+
122
+ ```python
123
+ import threading
124
+ from pubsub import Channel, publish, subscribe
125
+
126
+ topic = "broadcast"
127
+
128
+ # Create two independent channels for the same topic
129
+ channel1 = Channel(topic=topic)
130
+ channel2 = Channel(topic=topic)
131
+
132
+ with channel1, channel2:
133
+ # Start two subscriber threads
134
+ def subscriber1():
135
+ subscribe(channel1, lambda msg: print(f"Sub1: {msg.content}"), timeout_seconds=5.0)
136
+
137
+ def subscriber2():
138
+ subscribe(channel2, lambda msg: print(f"Sub2: {msg.content}"), timeout_seconds=5.0)
139
+
140
+ thread1 = threading.Thread(target=subscriber1)
141
+ thread2 = threading.Thread(target=subscriber2)
142
+ thread1.start()
143
+ thread2.start()
144
+
145
+ # Publish - both subscribers receive the message
146
+ publish(topic, b"Hello everyone!")
147
+
148
+ thread1.join()
149
+ thread2.join()
150
+ ```
151
+
152
+ ### Remote Procedure Call (RPC) Pattern
153
+
154
+ ```python
155
+ import threading
156
+ import time
157
+ from pubsub import Channel, publish, subscribe, fetch
158
+
159
+ # Server: Process requests and send responses
160
+ def rpc_server():
161
+ request_channel = Channel(topic="rpc.requests")
162
+
163
+ with request_channel:
164
+ def handle_request(request):
165
+ print(f"Server received: {request.content.decode()}")
166
+
167
+ # Extract response topic and correlation ID from headers
168
+ response_topic = request.headers.get("response-topic")
169
+ correlation_id = request.headers.get("correlation-id")
170
+
171
+ if response_topic and correlation_id:
172
+ # Process the request (simulate work)
173
+ result = f"Processed: {request.content.decode()}"
174
+
175
+ # Send response with correlation ID
176
+ response_headers = {
177
+ "correlation-id": correlation_id
178
+ }
179
+ publish(response_topic, result.encode(), headers=response_headers)
180
+ print(f"Server sent response with correlation-id: {correlation_id}")
181
+
182
+ subscribe(request_channel, handle_request, timeout_seconds=5.0)
183
+
184
+ # Client: Send request and wait for response
185
+ def rpc_client():
186
+ response_channel = Channel(topic="rpc.responses.client1")
187
+
188
+ with response_channel:
189
+ # Create request with response topic and correlation ID
190
+ request_data = b"Calculate 2 + 2"
191
+ request_headers = {
192
+ "response-topic": "rpc.responses.client1",
193
+ "correlation-id": str(int(time.time() * 1000000)) # Use timestamp as correlation ID
194
+ }
195
+
196
+ print(f"Client sending request with correlation-id: {request_headers['correlation-id']}")
197
+ publish("rpc.requests", request_data, headers=request_headers)
198
+
199
+ # Wait for response
200
+ response = fetch(response_channel)
201
+ if response:
202
+ correlation_id = response.headers.get("correlation-id")
203
+ print(f"Client received response with correlation-id: {correlation_id}")
204
+ print(f"Result: {response.content.decode()}")
205
+
206
+ # Start server in background thread
207
+ server_thread = threading.Thread(target=rpc_server)
208
+ server_thread.start()
209
+
210
+ # Give server time to start
211
+ time.sleep(0.1)
212
+
213
+ # Execute client request
214
+ rpc_client()
215
+
216
+ # Wait for server to finish
217
+ server_thread.join()
218
+ ```
219
+
220
+ ## API Reference
221
+
222
+ ### Channel
223
+
224
+ Represents a topic subscription point with a dedicated FIFO queue.
225
+
226
+ ```python
227
+ Channel(topic: str)
228
+ ```
229
+
230
+ **Parameters:**
231
+ - `topic` (str): Topic string with dots separating terms. Supports wildcards `=` (single word) and `+` (multiple words). Valid characters: `[a-zA-Z0-9+=.-]`
232
+
233
+ **Methods:**
234
+ - `open()`: Opens the FIFO queue for reading (called automatically by context manager)
235
+ - `close()`: Closes the queue and cleans up resources
236
+ - `__enter__()`, `__exit__()`: Context manager support
237
+
238
+ **Usage:**
239
+ ```python
240
+ channel = Channel(topic="app.logs")
241
+ with channel:
242
+ # Channel is open and ready
243
+ pass
244
+ # Channel is automatically closed
245
+ ```
246
+
247
+ ### publish()
248
+
249
+ Publishes a message to all matching channels.
250
+
251
+ ```python
252
+ publish(topic: str, data: bytes, headers: dict = None) -> int
253
+ ```
254
+
255
+ **Parameters:**
256
+ - `topic` (str): Topic to publish to (only alphanumeric characters, dots, and hyphens allowed: `[a-zA-Z0-9.-]`)
257
+ - `data` (bytes): Message payload
258
+ - `headers` (dict): Optional dictionary of string key-value pairs for metadata
259
+
260
+ **Returns:**
261
+ - `int`: Number of channels the message was published to
262
+
263
+ **Raises:**
264
+ - `ValueError`: If topic contains invalid characters (only `[a-zA-Z0-9.-]` allowed)
265
+
266
+ **Example with headers:**
267
+ ```python
268
+ from pubsub import publish
269
+
270
+ # Publish with custom headers
271
+ headers = {
272
+ "priority": "high",
273
+ "correlation-id": "12345"
274
+ }
275
+ publish("app.events", b"Event data", headers=headers)
276
+ ```
277
+
278
+ ### fetch()
279
+
280
+ Fetches a single message from a channel (non-blocking).
281
+
282
+ ```python
283
+ fetch(channel: Channel) -> Optional[Message]
284
+ ```
285
+
286
+ **Parameters:**
287
+ - `channel` (Channel): Open channel to fetch from
288
+
289
+ **Returns:**
290
+ - `Message`: Message object if available, `None` if queue is empty
291
+
292
+ **Note:** The channel must be opened (use `with channel:`) before calling `fetch()`.
293
+
294
+ ### subscribe()
295
+
296
+ Subscribes to a channel and processes messages with a callback.
297
+
298
+ ```python
299
+ subscribe(channel: Channel, callback: Callable[[Message], None], timeout_seconds: float = 0) -> int
300
+ ```
301
+
302
+ **Parameters:**
303
+ - `channel` (Channel): Open channel to subscribe to
304
+ - `callback` (Callable): Function called for each message received
305
+ - `timeout_seconds` (float): How long to listen (0 = indefinite)
306
+
307
+ **Returns:**
308
+ - `int`: Number of messages processed
309
+
310
+ **Raises:**
311
+ - `ValueError`: If timeout is negative
312
+
313
+ **Note:** The channel must be opened (use `with channel:`) before calling `subscribe()`.
314
+
315
+ ### Message
316
+
317
+ Represents a pub/sub message.
318
+
319
+ **Attributes:**
320
+ - `id` (int): Unique message identifier (timestamp-based with random bits)
321
+ - `timestamp` (int): Message creation timestamp in microseconds
322
+ - `topic` (str): Message topic
323
+ - `content` (bytes): Message payload
324
+ - `content_length` (int): Length of content in bytes
325
+ - `headers` (dict): Dictionary of string key-value pairs containing message metadata
326
+
327
+ ## Configuration
328
+
329
+ ### Environment Variables
330
+
331
+ #### PUBSUB_HOME
332
+
333
+ Override the default storage location for pubsub channels and messages.
334
+
335
+ **Default behavior:**
336
+ - Linux/Unix: `/dev/shm/pubsub` (tmpfs for best performance)
337
+ - Other systems: `<system_temp>/pubsub`
338
+
339
+ **Usage:**
340
+ ```bash
341
+ # Set custom storage location
342
+ export PUBSUB_HOME=/tmp/my-pubsub
343
+
344
+ # Run your application
345
+ python your_app.py
346
+ ```
347
+
348
+ **Example:**
349
+ ```python
350
+ import os
351
+ os.environ['PUBSUB_HOME'] = '/path/to/custom/location'
352
+
353
+ from pubsub import Channel, publish, subscribe
354
+
355
+ # Now all channels will use the custom location
356
+ channel = Channel(topic="app.logs")
357
+ ```
358
+
359
+ **Note:** The base directory is cached after first use, so `PUBSUB_HOME` should be set before importing or using pubsub functions.
360
+
361
+ ## Architecture
362
+
363
+ ### Storage Location
364
+
365
+ Messages are stored in `/dev/shm/pubsub/` (tmpfs) by default for fast access. Each channel creates a directory containing:
366
+ - `queue`: FIFO pipe for message IDs
367
+ - `<message_id>`: Message content files
368
+
369
+ ### Message Flow
370
+
371
+ 1. **Publish**: Message written to tmp, then hard-linked to each matching channel directory, ID written to FIFO
372
+ 2. **Fetch/Subscribe**: Read ID from FIFO, load message from file, delete message file
373
+ 3. **Cleanup**: Channel cleanup removes directory and all unconsumed messages
374
+
375
+ ### Wildcards
376
+
377
+ - `=` matches a single word: `logs.=.error` matches `logs.app.error` but not `logs.error`
378
+ - `+` matches one or more words: `logs.+` matches `logs.error`, `logs.app.error`, `logs.app.module.error`
379
+
380
+ Wildcards are converted to regex patterns for matching.
381
+
382
+ ## Testing
383
+
384
+ Run the test suite:
385
+
386
+ ```bash
387
+ # All tests
388
+ python -m unittest discover -s tests
389
+
390
+ # Specific test class
391
+ python -m unittest tests.test_pubsub.TestPublish -v
392
+
393
+ # Specific test
394
+ python -m unittest tests.test_channel.TestChannel.test_channel_creation -v
395
+ ```
396
+
397
+ ## Requirements
398
+
399
+ - Python 3.12+
400
+ - Linux/Unix with tmpfs support (`/dev/shm`)
401
+ - FIFO (named pipes) support
402
+
403
+ ## License
404
+
405
+ See LICENSE file for details.