messagex-client 0.1.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 MessageX contributors
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,664 @@
1
+ Metadata-Version: 2.4
2
+ Name: messagex-client
3
+ Version: 0.1.0
4
+ Summary: Official Python (asyncio) client for the MessageX message broker
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/sameeryadav05/MessageX/tree/main/packages/python-sdk
7
+ Project-URL: Repository, https://github.com/sameeryadav05/MessageX
8
+ Project-URL: Issues, https://github.com/sameeryadav05/MessageX/issues
9
+ Keywords: messagex,message-broker,queue,pubsub,messaging,tcp,asyncio,event-driven
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Networking
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # MessageX Python SDK
27
+
28
+ The official **asyncio** client for the [MessageX](https://github.com/sameeryadav05/MessageX) self-hosted message broker.
29
+
30
+ [![PyPI](https://img.shields.io/pypi/v/messagex-client)](https://pypi.org/project/messagex-client/)
31
+ [![Python](https://img.shields.io/badge/Python-%3E%3D3.9-3776AB?logo=python&logoColor=white)](https://www.python.org/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
33
+
34
+ Zero runtime dependencies · fully typed (`py.typed`) · same protocol (v1) as the [JavaScript SDK](https://www.npmjs.com/package/messagex-client).
35
+
36
+ ## Contents
37
+
38
+ - [Installation](#installation)
39
+ - [Quick start](#quick-start-5-minutes)
40
+ - [Core concepts](#core-concepts)
41
+ - [Connecting](#connecting)
42
+ - [Declaring queues and exchanges](#declaring-queues-and-exchanges)
43
+ - [Publishing messages](#publishing-messages)
44
+ - [Consuming messages](#consuming-messages)
45
+ - [Routing with exchange types](#routing-with-exchange-types)
46
+ - [Retries and dead-letter queues](#retries-and-dead-letter-queues)
47
+ - [Events](#events)
48
+ - [Error handling](#error-handling)
49
+ - [Recipes](#recipes) (auto-reconnect, graceful shutdown, FastAPI, logging)
50
+ - [Troubleshooting](#troubleshooting)
51
+ - [API reference](#api-reference)
52
+ - [Security and limitations](#security-and-limitations)
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install messagex-client
60
+ ```
61
+
62
+ The package name on PyPI is `messagex-client`; you import it as `messagex`:
63
+
64
+ ```python
65
+ from messagex import MessageX
66
+ ```
67
+
68
+ Requires Python 3.9+ and a running MessageX broker (the standard Docker deployment listens on `localhost:5678`).
69
+
70
+ > **This is an asyncio library.** Every network call is a coroutine, so you `await` it inside an `async def` function and start your program with `asyncio.run(...)`. In Jupyter/IPython you can use top-level `await` directly.
71
+
72
+ ---
73
+
74
+ ## Quick start (5 minutes)
75
+
76
+ Set your broker credentials once:
77
+
78
+ ```bash
79
+ export MESSAGEX_USER="your-username"
80
+ export MESSAGEX_PASSWORD="your-password"
81
+ ```
82
+
83
+ ### 1. Consumer: `consumer.py`
84
+
85
+ ```python
86
+ import asyncio
87
+ import os
88
+
89
+ from messagex import MessageX
90
+
91
+
92
+ async def handle(message):
93
+ print("Received:", message.body)
94
+ await message.ack() # tell the broker we're done with it
95
+
96
+
97
+ async def main():
98
+ async with MessageX(
99
+ host="127.0.0.1",
100
+ username=os.environ["MESSAGEX_USER"],
101
+ password=os.environ["MESSAGEX_PASSWORD"],
102
+ ) as client:
103
+ # Set up the topology (exchange -> binding -> queue)
104
+ await client.exchange.declare("orders.exchange", "direct", durable=True)
105
+ await client.queue.declare("orders", durable=True)
106
+ await client.exchange.bind("orders.exchange", "orders", "orders.created")
107
+
108
+ await client.consume("orders", handle)
109
+ print("Waiting for messages. Press Ctrl+C to stop.")
110
+ await asyncio.Event().wait() # run forever
111
+
112
+
113
+ if __name__ == "__main__":
114
+ try:
115
+ asyncio.run(main())
116
+ except KeyboardInterrupt:
117
+ pass
118
+ ```
119
+
120
+ ### 2. Producer: `producer.py`
121
+
122
+ ```python
123
+ import asyncio
124
+ import os
125
+
126
+ from messagex import MessageX
127
+
128
+
129
+ async def main():
130
+ async with MessageX(
131
+ host="127.0.0.1",
132
+ username=os.environ["MESSAGEX_USER"],
133
+ password=os.environ["MESSAGEX_PASSWORD"],
134
+ ) as client:
135
+ result = await client.publish(
136
+ "orders.exchange",
137
+ {"orderId": "ORD-1001", "amount": 2500, "status": "created"},
138
+ routing_key="orders.created",
139
+ headers={"source": "checkout-service"},
140
+ )
141
+ print("Routed to", result.routed_queues, "queue(s)")
142
+
143
+
144
+ asyncio.run(main())
145
+ ```
146
+
147
+ Run `python consumer.py` in one terminal, then `python producer.py` in another. The consumer prints the order.
148
+
149
+ ---
150
+
151
+ ## Core concepts
152
+
153
+ ```
154
+ Producer ──publish──▶ Exchange ──binding (routing key)──▶ Queue ──deliver──▶ Consumer
155
+ "orders.exchange" "orders.created" "orders" ack / nack
156
+ ```
157
+
158
+ | Concept | What it is |
159
+ |---|---|
160
+ | **Exchange** | Receives published messages and decides which queue(s) get them. Has a type: `direct`, `fanout`, `topic` or `headers`. |
161
+ | **Queue** | Holds messages until a consumer processes them. |
162
+ | **Binding** | A rule linking an exchange to a queue, usually with a *routing key*. |
163
+ | **Routing key** | A label on each published message that bindings match against. |
164
+ | **Consumer** | Your code, subscribed to a queue. Each delivered message must be **acked** (done) or **nacked** (failed). |
165
+
166
+ A message is **published to an exchange** (never directly to a queue). If no binding matches, `result.routed_queues` is `0`.
167
+
168
+ ---
169
+
170
+ ## Connecting
171
+
172
+ ```python
173
+ client = MessageX(
174
+ host="127.0.0.1", # required
175
+ username="user", # required
176
+ password="secret", # required
177
+ port=5678, # default 5678
178
+ client_name="billing-service",
179
+ connect_timeout=10.0, # seconds
180
+ request_timeout=15.0, # seconds
181
+ max_frame_size=1_048_576, # bytes
182
+ )
183
+ ```
184
+
185
+ ### Option A: context manager (recommended)
186
+
187
+ ```python
188
+ async with MessageX(host=..., username=..., password=...) as client:
189
+ ... # connected and authenticated here
190
+ # closed automatically, even if an exception was raised
191
+ ```
192
+
193
+ ### Option B: manual lifecycle
194
+
195
+ ```python
196
+ client = MessageX(host=..., username=..., password=...)
197
+ await client.connect()
198
+ try:
199
+ ...
200
+ finally:
201
+ await client.close()
202
+ ```
203
+
204
+ `connect()` is safe to call more than once; concurrent calls share a single TCP connection. Check state with `client.is_connected` and `client.is_authenticated`.
205
+
206
+ ### Reading config from the environment
207
+
208
+ ```python
209
+ import os
210
+ from messagex import MessageX
211
+
212
+ def make_client() -> MessageX:
213
+ return MessageX(
214
+ host=os.environ.get("MESSAGEX_HOST", "127.0.0.1"),
215
+ port=int(os.environ.get("MESSAGEX_PORT", "5678")),
216
+ username=os.environ["MESSAGEX_USER"],
217
+ password=os.environ["MESSAGEX_PASSWORD"],
218
+ )
219
+ ```
220
+
221
+ Never hard-code credentials in source control.
222
+
223
+ ---
224
+
225
+ ## Declaring queues and exchanges
226
+
227
+ Declare what you need when your service starts.
228
+
229
+ ```python
230
+ await client.queue.declare(
231
+ "orders",
232
+ durable=True, # survive broker restarts
233
+ exclusive=False,
234
+ auto_delete=False,
235
+ max_delivery_count=5, # see "Retries and dead-letter queues"
236
+ dead_letter_queue="orders.dead",
237
+ )
238
+
239
+ await client.exchange.declare("orders.exchange", "topic", durable=True)
240
+
241
+ await client.exchange.bind("orders.exchange", "orders", "orders.*")
242
+ ```
243
+
244
+ Options you leave out (or pass as `None`) are not sent, so the broker applies its own defaults.
245
+
246
+ To remove things:
247
+
248
+ ```python
249
+ await client.exchange.unbind("orders.exchange", "orders", "orders.*")
250
+ await client.queue.delete("orders")
251
+ await client.exchange.delete("orders.exchange")
252
+ ```
253
+
254
+ These calls return the broker's response as a plain `dict` using the broker's key names, for example:
255
+
256
+ ```python
257
+ info = await client.queue.declare("orders", durable=True)
258
+ print(info["queue"]["name"])
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Publishing messages
264
+
265
+ ```python
266
+ result = await client.publish(
267
+ "orders.exchange", # exchange name
268
+ {"orderId": "ORD-1001", "amount": 2500}, # message: any JSON-serializable value
269
+ routing_key="orders.created", # optional, default ""
270
+ headers={"source": "checkout-service"}, # optional metadata
271
+ )
272
+ print(result.exchange_name, result.routed_queues)
273
+ ```
274
+
275
+ - The message can be a `dict`, `list`, `str`, number, `bool` or `None`.
276
+ - Non-JSON values (for example `datetime`, `Decimal`, `set`, custom objects) raise `TypeError`. Convert them first, e.g. `order["created"] = created.isoformat()`.
277
+ - A message larger than `max_frame_size` raises `ValueError`.
278
+ - `routed_queues == 0` means the broker accepted the message but no queue was bound to receive it.
279
+
280
+ ---
281
+
282
+ ## Consuming messages
283
+
284
+ ```python
285
+ async def handle(message):
286
+ ...
287
+
288
+ subscription = await client.consume("orders", handle)
289
+ ```
290
+
291
+ `consume()` returns a `ConsumerSubscription` with `consumer_tag`, `queue_name` and `await cancel()`.
292
+
293
+ ### The message object
294
+
295
+ | Attribute / method | Description |
296
+ |---|---|
297
+ | `message.body` | The decoded message (what the producer published) |
298
+ | `message.headers` | Headers dict |
299
+ | `message.routing_key` | Routing key it was published with |
300
+ | `message.message_id` | Broker message id |
301
+ | `message.delivery_count` | How many times this message has been delivered |
302
+ | `message.created_at` | Creation timestamp (string) |
303
+ | `message.queue_name`, `message.consumer_tag`, `message.delivery_tag` | Delivery identifiers |
304
+ | `await message.ack()` | Success: the message is done |
305
+ | `await message.nack(requeue=True)` | Failure: put it back (`True`) or reject it (`False`) |
306
+
307
+ ### Always ack or nack
308
+
309
+ ```python
310
+ async def handle(message):
311
+ try:
312
+ await process_order(message.body)
313
+ await message.ack()
314
+ except TemporaryError:
315
+ await message.nack(requeue=True) # try again later
316
+ except Exception:
317
+ await message.nack(requeue=False) # give up on this message
318
+ raise
319
+ ```
320
+
321
+ The SDK **never** acks or nacks for you. The broker currently uses a prefetch of 1 per consumer connection, so an unsettled message can hold up the ones behind it.
322
+
323
+ ### Sync or async handlers
324
+
325
+ Handlers can be `async def` (recommended, since you can `await message.ack()` inside) or a plain function that returns an awaitable:
326
+
327
+ ```python
328
+ def handle(message):
329
+ return message.ack() # returning the awaitable is enough
330
+ ```
331
+
332
+ A plain synchronous handler runs on the event loop, so keep it fast. Do not call blocking code (`time.sleep`, `requests`, heavy CPU work) inside a handler. Use `await asyncio.to_thread(blocking_fn, ...)` for that.
333
+
334
+ If your handler raises, the SDK emits a [`consumer_error`](#events) event and carries on with the next message.
335
+
336
+ ### Stopping a consumer
337
+
338
+ ```python
339
+ await subscription.cancel() # stop receiving, keep the connection open
340
+ await client.close() # or close everything
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Routing with exchange types
346
+
347
+ | Type | Delivers a message to... |
348
+ |---|---|
349
+ | `direct` | queues bound with a routing key equal to the message's routing key |
350
+ | `fanout` | every bound queue (routing key not used for matching) |
351
+ | `topic` | queues whose binding pattern matches the routing key, e.g. `orders.*` |
352
+ | `headers` | queues whose binding `arguments` match the message headers |
353
+
354
+ ### Direct: one key, one destination
355
+
356
+ ```python
357
+ await client.exchange.declare("tasks", "direct")
358
+ await client.queue.declare("emails")
359
+ await client.exchange.bind("tasks", "emails", "send.email")
360
+ await client.publish("tasks", {"to": "a@b.com"}, routing_key="send.email")
361
+ ```
362
+
363
+ ### Fanout: broadcast to everyone
364
+
365
+ ```python
366
+ await client.exchange.declare("events", "fanout")
367
+ for name in ("audit", "analytics", "notifications"):
368
+ await client.queue.declare(name)
369
+ await client.exchange.bind("events", name) # no routing key needed
370
+ await client.publish("events", {"type": "user.signup"}) # copied to all three queues
371
+ ```
372
+
373
+ ### Topic: pattern matching
374
+
375
+ ```python
376
+ await client.exchange.declare("orders.exchange", "topic")
377
+ await client.queue.declare("all-orders")
378
+ await client.exchange.bind("orders.exchange", "all-orders", "orders.*")
379
+ await client.publish("orders.exchange", {"id": 1}, routing_key="orders.created")
380
+ await client.publish("orders.exchange", {"id": 2}, routing_key="orders.cancelled")
381
+ ```
382
+
383
+ ### Headers: match on metadata
384
+
385
+ ```python
386
+ await client.exchange.declare("reports", "headers")
387
+ await client.exchange.bind("reports", "pdf-queue", arguments={"format": "pdf"})
388
+ await client.publish("reports", {"id": 7}, headers={"format": "pdf"})
389
+ ```
390
+
391
+ The exact matching rules for each type are defined by the broker. See the [MessageX documentation](https://github.com/sameeryadav05/MessageX#readme) for details, especially for `headers` bindings.
392
+
393
+ ### Work queue: scale out by running more consumers
394
+
395
+ Run the same consumer script in several processes against the same queue to spread the work.
396
+
397
+ ---
398
+
399
+ ## Retries and dead-letter queues
400
+
401
+ Queues accept `max_delivery_count` and `dead_letter_queue`, so messages that keep failing can be routed to a separate queue instead of looping forever:
402
+
403
+ ```python
404
+ await client.queue.declare("orders.dead", durable=True)
405
+ await client.queue.declare(
406
+ "orders",
407
+ durable=True,
408
+ max_delivery_count=5,
409
+ dead_letter_queue="orders.dead",
410
+ )
411
+ ```
412
+
413
+ Use `message.delivery_count` to see which attempt you are on:
414
+
415
+ ```python
416
+ async def handle(message):
417
+ try:
418
+ await process(message.body)
419
+ await message.ack()
420
+ except Exception:
421
+ print(f"attempt {message.delivery_count} failed")
422
+ await message.nack(requeue=True)
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Events
428
+
429
+ The client emits events you can subscribe to with `on`, `once` and `off`. Listeners can be sync or async.
430
+
431
+ | Event | Arguments | When |
432
+ |---|---|---|
433
+ | `connect` | none | Connected and authenticated |
434
+ | `disconnect` | none | The connection dropped unexpectedly (not emitted by your own `close()`) |
435
+ | `close` | none | `close()` finished |
436
+ | `message` | `DeliveredMessage` | Any message delivered (in addition to your handler) |
437
+ | `event` | `dict` | Other broker events |
438
+ | `consumer_error` | `exception, message` | Your handler raised |
439
+ | `warning` | `Exception` | Non-fatal problem (e.g. delivery for an unknown consumer) |
440
+ | `error` | `Exception` | Connection or protocol error |
441
+
442
+ ```python
443
+ client.on("disconnect", lambda: print("connection lost!"))
444
+ client.on("consumer_error", lambda exc, msg: print(f"handler failed on {msg.message_id}: {exc!r}"))
445
+ client.on("warning", lambda err: print("warning:", err))
446
+ ```
447
+
448
+ If there is no `error` listener, errors are emitted as `warning`. If there is no `warning` listener either, they are written to the `messagex` logger.
449
+
450
+ ---
451
+
452
+ ## Error handling
453
+
454
+ All SDK errors inherit from `MessageXError`:
455
+
456
+ | Exception | Meaning |
457
+ |---|---|
458
+ | `MessageXConnectionError` | Can't connect, not connected/authenticated, or the connection was lost |
459
+ | `MessageXTimeoutError` | Connect or a command exceeded its timeout |
460
+ | `MessageXBrokerError` | The broker rejected the command; has `.code` and `.message` |
461
+
462
+ ```python
463
+ from messagex import (
464
+ MessageXBrokerError, MessageXConnectionError, MessageXTimeoutError,
465
+ )
466
+
467
+ try:
468
+ await client.queue.declare("orders", durable=True)
469
+ except MessageXBrokerError as err:
470
+ print("Broker said no:", err.code, err)
471
+ except MessageXTimeoutError:
472
+ print("Broker too slow")
473
+ except MessageXConnectionError as err:
474
+ print("Connection problem:", err)
475
+ ```
476
+
477
+ Argument mistakes raise standard Python errors: `TypeError` (missing credentials, non-JSON message) or `ValueError` (message too large).
478
+
479
+ ---
480
+
481
+ ## Recipes
482
+
483
+ ### Auto-reconnect
484
+
485
+ The SDK does **not** reconnect or restore subscriptions by itself. After a disconnect you must connect and `consume()` again. A simple, robust pattern is to build a fresh client in a loop:
486
+
487
+ ```python
488
+ import asyncio
489
+ from messagex import MessageX, MessageXConnectionError, MessageXTimeoutError
490
+
491
+ async def run_consumer(stop: asyncio.Event):
492
+ delay = 1.0
493
+ while not stop.is_set():
494
+ client = make_client() # see "Reading config from the environment"
495
+ lost = asyncio.Event()
496
+ client.on("disconnect", lambda: lost.set())
497
+ try:
498
+ await client.connect()
499
+ await client.consume("orders", handle)
500
+ delay = 1.0 # healthy again: reset backoff
501
+
502
+ waiter = asyncio.ensure_future(lost.wait())
503
+ stopper = asyncio.ensure_future(stop.wait())
504
+ await asyncio.wait({waiter, stopper}, return_when=asyncio.FIRST_COMPLETED)
505
+ waiter.cancel()
506
+ stopper.cancel()
507
+ except (MessageXConnectionError, MessageXTimeoutError) as exc:
508
+ print("connection failed:", exc)
509
+ finally:
510
+ await client.close()
511
+
512
+ if not stop.is_set():
513
+ await asyncio.sleep(delay)
514
+ delay = min(delay * 2, 30.0) # exponential backoff, max 30 s
515
+ ```
516
+
517
+ ### Graceful shutdown (Ctrl+C / SIGTERM)
518
+
519
+ ```python
520
+ import asyncio, signal
521
+
522
+ async def main():
523
+ stop = asyncio.Event()
524
+ loop = asyncio.get_running_loop()
525
+ for sig in (signal.SIGINT, signal.SIGTERM):
526
+ loop.add_signal_handler(sig, stop.set) # not available on Windows
527
+
528
+ async with make_client() as client:
529
+ subscription = await client.consume("orders", handle)
530
+ await stop.wait()
531
+ await subscription.cancel() # stop taking new work, then exit
532
+ ```
533
+
534
+ ### Publishing from a FastAPI app
535
+
536
+ Create one client at startup and reuse it for every request:
537
+
538
+ ```python
539
+ from contextlib import asynccontextmanager
540
+ from fastapi import FastAPI, Request
541
+
542
+ @asynccontextmanager
543
+ async def lifespan(app: FastAPI):
544
+ client = make_client()
545
+ await client.connect()
546
+ app.state.mx = client
547
+ yield
548
+ await client.close()
549
+
550
+ app = FastAPI(lifespan=lifespan)
551
+
552
+ @app.post("/orders")
553
+ async def create_order(order: dict, request: Request):
554
+ result = await request.app.state.mx.publish(
555
+ "orders.exchange", order, routing_key="orders.created"
556
+ )
557
+ return {"queued": result.routed_queues > 0}
558
+ ```
559
+
560
+ ### Publishing from synchronous code
561
+
562
+ If your code is not async, run a short-lived call with `asyncio.run`:
563
+
564
+ ```python
565
+ import asyncio
566
+
567
+ def publish_once(payload: dict) -> None:
568
+ async def _go():
569
+ async with make_client() as client:
570
+ await client.publish("orders.exchange", payload, routing_key="orders.created")
571
+ asyncio.run(_go())
572
+ ```
573
+
574
+ This opens a new connection per call, so use it for scripts and occasional jobs, not high-volume publishing. Don't call `asyncio.run` from inside an already-running event loop.
575
+
576
+ ### Logging
577
+
578
+ ```python
579
+ import logging
580
+ logging.basicConfig(level=logging.INFO)
581
+ logging.getLogger("messagex").setLevel(logging.DEBUG)
582
+ ```
583
+
584
+ ---
585
+
586
+ ## Troubleshooting
587
+
588
+ | Symptom | Likely cause and fix |
589
+ |---|---|
590
+ | `MessageXConnectionError: Could not connect to MessageX at host:port` | Broker not running, wrong host/port, or firewall. Check `docker ps` and the port (default `5678`). |
591
+ | `MessageXTimeoutError` while connecting | Host unreachable, or something other than MessageX is listening on that port. Raise `connect_timeout` if the network is slow. |
592
+ | `MessageXBrokerError` right after connect | Wrong username/password. Inspect `err.code`. |
593
+ | `MessageXConnectionError: ... Call connect() first` | You used the client before `connect()` or after `close()`. |
594
+ | `MessageXConnectionError: ... not authenticated` | A command was sent while `connect()` was still in progress. `await client.connect()` first. |
595
+ | `publish` works but `routed_queues` is `0` | No binding matches the routing key. Check exchange type, queue binding and routing key spelling. |
596
+ | Consumer gets one message and then stops | You didn't `ack()`/`nack()` it. Settle every message. |
597
+ | `TypeError: MessageX protocol message must be JSON serializable` | Convert `datetime`/`Decimal`/custom objects to plain JSON types before publishing. |
598
+ | `RuntimeWarning: coroutine ... was never awaited` | You forgot `await` on a client call (`await client.publish(...)`). |
599
+ | Handler seems to freeze all processing | A blocking call inside a handler is blocking the event loop. Use `await asyncio.to_thread(...)`. |
600
+ | `asyncio.run() cannot be called from a running event loop` | You're in Jupyter/an async framework. Use `await main()` instead. |
601
+
602
+ ---
603
+
604
+ ## API reference
605
+
606
+ ### `MessageX(host, username, password, port=5678, *, client_name, connect_timeout, request_timeout, max_frame_size)`
607
+
608
+ | Argument | Type | Default | Description |
609
+ |---|---|---:|---|
610
+ | `host` | `str` | required | Broker hostname or IP |
611
+ | `username` | `str` | required | Broker username |
612
+ | `password` | `str` | required | Broker password |
613
+ | `port` | `int` | `5678` | Broker TCP port |
614
+ | `client_name` | `str` | `"messagex-python-client"` | Client identifier |
615
+ | `connect_timeout` | `float` | `10.0` | TCP connect timeout (seconds) |
616
+ | `request_timeout` | `float` | `15.0` | Per-command timeout (seconds) |
617
+ | `max_frame_size` | `int` | `1048576` | Max frame body size (bytes) |
618
+
619
+ ### Methods
620
+
621
+ | Method | Returns |
622
+ |---|---|
623
+ | `await connect()` | `None` |
624
+ | `await close()` | `None` |
625
+ | `await publish(exchange_name, message, routing_key="", headers=None)` | `PublishResult(exchange_name, routed_queues)` |
626
+ | `await consume(queue_name, on_message)` | `ConsumerSubscription` |
627
+ | `await queue.declare(name, *, durable, exclusive, auto_delete, max_delivery_count, dead_letter_queue)` | `dict` |
628
+ | `await queue.delete(queue_name)` | `dict` |
629
+ | `await exchange.declare(name, type, *, durable)` | `dict` |
630
+ | `await exchange.delete(exchange_name)` | `dict` |
631
+ | `await exchange.bind(exchange_name, queue_name, routing_key="", arguments=None)` | `dict` |
632
+ | `await exchange.unbind(exchange_name, queue_name, routing_key="", arguments=None)` | `dict` |
633
+ | `on(event, fn)` / `once(event, fn)` / `off(event, fn)` | the client |
634
+
635
+ ### Properties
636
+
637
+ `is_connected`, `is_authenticated`
638
+
639
+ ### Exports
640
+
641
+ `MessageX`, `MessageXError`, `MessageXConnectionError`, `MessageXTimeoutError`, `MessageXBrokerError`, `DeliveredMessage`, `ConsumerSubscription`, `PublishResult`, `ExchangeType`, `MessageHandler`
642
+
643
+ ---
644
+
645
+ ## Security and limitations
646
+
647
+ - The broker's TCP protocol currently has **no TLS**. Don't send credentials or messages over untrusted networks or expose port `5678` to the public internet. Use a private network, VPN or SSH tunnel, and firewall rules.
648
+ - No automatic reconnect or subscription recovery (see the [recipe](#auto-reconnect)).
649
+ - Not provided: TLS transport, transactions, batch publishing, management HTTP API, a blocking (non-asyncio) API.
650
+ - Implements protocol v1: CONNECT, AUTH, QUEUE_DECLARE, QUEUE_DELETE, EXCHANGE_DECLARE, EXCHANGE_DELETE, BIND, UNBIND, PUBLISH, CONSUME, ACK, NACK, CANCEL.
651
+
652
+ ## Development
653
+
654
+ ```bash
655
+ git clone https://github.com/sameeryadav05/MessageX && cd MessageX/packages/python-sdk
656
+ pip install -e .
657
+ PYTHONPATH=src:tests python -m unittest discover -s tests -v
658
+ ```
659
+
660
+ End-to-end example against a running broker: [`examples/real_broker_test.py`](examples/real_broker_test.py).
661
+
662
+ ## License
663
+
664
+ MIT