amqp-client-python 0.2.1.dev1__py3-none-any.whl → 0.2.2.dev1__py3-none-any.whl

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.
@@ -15,6 +15,12 @@ class Options:
15
15
  vhost: str = "/",
16
16
  heartbeat: Optional[int] = 60,
17
17
  publisher_confirms: bool = False,
18
+ rpc_exchange_type: str = "topic",
19
+ rpc_queue_durable: bool = True,
20
+ rpc_queue_auto_delete: bool = False,
21
+ exchange_type: str = "topic",
22
+ queue_durable: bool = True,
23
+ queue_auto_delete: bool = False,
18
24
  **kwargs: Dict[str, Any]
19
25
  ) -> None:
20
26
  """
@@ -32,6 +38,12 @@ class Options:
32
38
  vhost: vhost information
33
39
  heartbeat: interval between heartbeat
34
40
  publisher_confirms: wait for publisher confirmations
41
+ rpc_exchange_type: exchange type for RPC server (default 'topic')
42
+ rpc_queue_durable: durability for RPC server queue (default True)
43
+ rpc_queue_auto_delete: auto_delete for RPC server queue (default False)
44
+ exchange_type: exchange type for pub/sub subscriptions (default 'topic')
45
+ queue_durable: durability for pub/sub subscriptions queue (default True)
46
+ queue_auto_delete: auto_delete for pub/sub subscriptions queue (default False)
35
47
  **kwargs: pika options
36
48
 
37
49
  Examples:
@@ -56,4 +68,10 @@ class Options:
56
68
  self.vhost = vhost
57
69
  self.heartbeat = heartbeat
58
70
  self.publisher_confirms = publisher_confirms
71
+ self.rpc_exchange_type = kwargs.pop("rpc_exchange_type", rpc_exchange_type)
72
+ self.rpc_queue_durable = kwargs.pop("rpc_queue_durable", rpc_queue_durable)
73
+ self.rpc_queue_auto_delete = kwargs.pop("rpc_queue_auto_delete", rpc_queue_auto_delete)
74
+ self.exchange_type = kwargs.pop("exchange_type", exchange_type)
75
+ self.queue_durable = kwargs.pop("queue_durable", queue_durable)
76
+ self.queue_auto_delete = kwargs.pop("queue_auto_delete", queue_auto_delete)
59
77
  self.kwargs = kwargs
@@ -1,5 +1,5 @@
1
1
  from collections import deque
2
- from typing import MutableMapping, Mapping, Optional, Union, Dict, List
2
+ from typing import MutableMapping, Mapping, Optional, Union, Dict, List, Tuple, Any
3
3
  from .async_channel_factory import AsyncChannelFactoryRabbitMQ
4
4
  from amqp_client_python.exceptions import (
5
5
  NackException,
@@ -54,11 +54,20 @@ class AsyncChannel:
54
54
  self.auto_ack = auto_ack
55
55
  self.subscribes: Dict[str, Dict[str, HandlerType]] = {}
56
56
  self.re_subscribes: List[str] = []
57
+ self._compiled_patterns: Dict[str, List[Tuple[str, Any]]] = {}
58
+ self._queue_exchange_type: Dict[str, str] = {}
57
59
  self.consumers: Dict[str, bool] = {}
58
60
  self.publisher_confirms = False
59
61
  self._message_number = 0
60
62
  self._deliveries: MutableMapping[int, Future] = {}
61
63
  self.response_timeout = 60
64
+ self._declared_exchanges: Dict[str, Tuple[str, bool]] = {}
65
+
66
+ @staticmethod
67
+ def _serialize_payload(body, content_type: str = "application/json"):
68
+ if isinstance(body, (bytes, bytearray, memoryview)):
69
+ return body
70
+ return dumps(body)
62
71
 
63
72
  @property
64
73
  def is_open(self) -> Optional[bool]:
@@ -81,6 +90,7 @@ class AsyncChannel:
81
90
  """
82
91
  LOGGER.info("Channel opened")
83
92
  self._channel = channel
93
+ self._declared_exchanges.clear()
84
94
  self.add_on_channel_close_callback()
85
95
  self.publisher_confirms and self.add_publish_confirms()
86
96
  self._prefetch_count and self.set_qos(self._prefetch_count)
@@ -114,12 +124,14 @@ class AsyncChannel:
114
124
  :param pika.channel.Channel: The closed channel
115
125
  :param Exception reason: why the channel was closed
116
126
  """
117
- LOGGER.info(f"Channel {channel} was closed: {reason}")
127
+ LOGGER.warning(f"Channel {channel} was closed by RabbitMQ: {reason}")
118
128
  self._consuming = False
129
+ if hasattr(self, "_declared_exchanges"):
130
+ self._declared_exchanges.clear()
119
131
  if self._connection.is_closing or self._connection.is_closed:
120
132
  LOGGER.info("Connection is closing or already closed")
121
133
  else:
122
- LOGGER.info("Closing connection")
134
+ LOGGER.info(f"Closing connection due to channel closure: {reason}")
123
135
  self._connection.close()
124
136
 
125
137
  def add_publish_confirms(self):
@@ -154,14 +166,36 @@ class AsyncChannel:
154
166
  be invoked by pika.
155
167
  :param str|unicode exchange_name: The name of the exchange to declare
156
168
  """
157
- LOGGER.info(f"Declaring exchange: {exchange_name}")
169
+ if not exchange_name:
170
+ return
171
+
172
+ exchange_type_str = str(getattr(exchange_type, "value", exchange_type))
173
+ durable_bool = bool(durable)
174
+
175
+ if not hasattr(self, "_declared_exchanges"):
176
+ self._declared_exchanges = {}
177
+
178
+ if exchange_name in self._declared_exchanges:
179
+ prev_type, prev_durable = self._declared_exchanges[exchange_name]
180
+ if (prev_type, prev_durable) == (exchange_type_str, durable_bool):
181
+ return
182
+ LOGGER.warning(
183
+ f"Exchange '{exchange_name}' was already declared as type='{prev_type}', durable={prev_durable}. "
184
+ f"Re-declaring with type='{exchange_type_str}', durable={durable_bool} on the same channel may "
185
+ f"trigger PRECONDITION_FAILED."
186
+ )
187
+
188
+ self._declared_exchanges[exchange_name] = (exchange_type_str, durable_bool)
189
+ LOGGER.info(
190
+ f"Declaring exchange: {exchange_name} (type={exchange_type_str}, durable={durable_bool})"
191
+ )
158
192
  # Note: using functools.partial is not required, it is demonstrating
159
193
  # how arbitrary data can be passed to the callback when it is called
160
194
  cb = partial(self.on_exchange_declareok, userdata=exchange_name)
161
195
  self._channel.exchange_declare(
162
196
  exchange=exchange_name,
163
- exchange_type=exchange_type,
164
- durable=durable,
197
+ exchange_type=exchange_type_str,
198
+ durable=durable_bool,
165
199
  callback=cb,
166
200
  )
167
201
 
@@ -361,7 +395,7 @@ class AsyncChannel:
361
395
  self._channel.basic_publish(
362
396
  exchange_name,
363
397
  routing_key,
364
- dumps(body), # Serialize payload to JSON
398
+ self._serialize_payload(body, content_type),
365
399
  properties=BasicProperties(
366
400
  reply_to=self._callback_queue, # Response queue address
367
401
  correlation_id=corr_id, # Request/response matching ID
@@ -436,7 +470,7 @@ class AsyncChannel:
436
470
  self._channel.basic_publish(
437
471
  exchange_name,
438
472
  routing_key,
439
- dumps(body),
473
+ self._serialize_payload(body, content_type),
440
474
  properties=BasicProperties(
441
475
  reply_to=self._callback_queue,
442
476
  content_type=content_type,
@@ -491,13 +525,14 @@ class AsyncChannel:
491
525
  exchange_type="topic",
492
526
  durable=True,
493
527
  auto_delete=False,
528
+ auto_decode=True,
494
529
  ):
495
530
  await self.start_rpc_publisher()
496
531
  self.register_queue(
497
532
  exchange_name, queue_name, exchange_type, durable, auto_delete
498
533
  )
499
534
  self.register_handler(
500
- queue_name, routing_key, callback, content_type, timeout
535
+ queue_name, routing_key, callback, content_type, timeout, auto_decode=auto_decode
501
536
  )
502
537
 
503
538
  self.queue_bind(queue_name, exchange_name, routing_key)
@@ -518,30 +553,52 @@ class AsyncChannel:
518
553
  self,
519
554
  exchange_name,
520
555
  queue_name: str,
521
- exchange_type="direct",
556
+ exchange_type="topic",
522
557
  durable=True,
523
558
  auto_delete=False,
524
559
  ):
525
560
  if queue_name not in self.subscribes:
526
- if not len(self.subscribes):
527
- self.subscribes[queue_name] = {}
528
- self.setup_exchange(exchange_name, exchange_type)
561
+ self.subscribes[queue_name] = {}
562
+ exchange_type_str = str(getattr(exchange_type, "value", exchange_type))
563
+ if not hasattr(self, "_queue_exchange_type"):
564
+ self._queue_exchange_type = {}
565
+ self._queue_exchange_type[queue_name] = exchange_type_str
566
+
567
+ self.setup_exchange(exchange_name, exchange_type, durable=durable)
529
568
  self.queue_declare(queue_name, durable=durable, auto_delete=auto_delete)
530
569
 
531
570
  if queue_name not in self.consumers:
532
571
  self.consumers[queue_name] = False
533
572
 
573
+ def _compile_routing_pattern(self, queue_name: str, routing_key: str):
574
+ if not hasattr(self, "_compiled_patterns"):
575
+ self._compiled_patterns = {}
576
+ if queue_name not in self._compiled_patterns:
577
+ self._compiled_patterns[queue_name] = []
578
+
579
+ if "*" in routing_key or "#" in routing_key:
580
+ # AMQP topic wildcard pattern (* = single word [^.]+, # = zero or more words .*)
581
+ amqp_regex = "^" + re.escape(routing_key).replace(r"\*", r"[^.]+").replace(r"\#", r".*") + "$"
582
+ try:
583
+ self._compiled_patterns[queue_name].append((routing_key, re.compile(amqp_regex)))
584
+ except re.error:
585
+ pass
586
+
534
587
  def register_handler(
535
- self, queue_name, routing_key, callback, content_type, response_timeout
588
+ self, queue_name, routing_key, callback, content_type, response_timeout, auto_decode=True
536
589
  ):
590
+ if queue_name not in self.subscribes:
591
+ self.subscribes[queue_name] = {}
537
592
  if routing_key not in self.subscribes[queue_name]:
538
593
  self.subscribes[queue_name][routing_key] = {
539
594
  "handle": callback,
540
595
  "content_type": content_type,
541
596
  "response_timeout": response_timeout or self.response_timeout,
597
+ "auto_decode": auto_decode,
542
598
  }
543
599
  if "*" in routing_key and routing_key not in self.re_subscribes:
544
600
  self.re_subscribes.append(routing_key)
601
+ self._compile_routing_pattern(queue_name, routing_key)
545
602
 
546
603
  async def subscribe(
547
604
  self,
@@ -554,12 +611,13 @@ class AsyncChannel:
554
611
  exchange_type: str = "topic",
555
612
  durable: bool = True,
556
613
  auto_delete: bool = False,
614
+ auto_decode: bool = True,
557
615
  ) -> None:
558
616
  self.register_queue(
559
617
  exchange_name, queue_name, exchange_type, durable, auto_delete
560
618
  )
561
619
  self.register_handler(
562
- queue_name, routing_key, callback, content_type, timeout
620
+ queue_name, routing_key, callback, content_type, timeout, auto_decode=auto_decode
563
621
  )
564
622
 
565
623
  self.queue_bind(queue_name, exchange_name, routing_key)
@@ -587,7 +645,11 @@ class AsyncChannel:
587
645
 
588
646
  async def process_message(handler: HandlerType):
589
647
  nonlocal body
590
- body = loads(body)
648
+ if handler.get("auto_decode", True) and handler.get("content_type") == "application/json":
649
+ try:
650
+ body = loads(body)
651
+ except Exception:
652
+ pass
591
653
  response = await wait_for(
592
654
  handler["handle"](body),
593
655
  timeout=handler["response_timeout"],
@@ -609,30 +671,39 @@ class AsyncChannel:
609
671
 
610
672
  async def handle_message(queue_name, basic_deliver, props):
611
673
  try:
612
- if basic_deliver.routing_key in self.subscribes[queue_name]:
613
- await process_message(
614
- self.subscribes[queue_name][basic_deliver.routing_key]
615
- )
616
- else:
617
- for routing_key in self.re_subscribes:
618
- if re.match(routing_key, basic_deliver.routing_key):
619
- return await process_message(
620
- self.subscribes[queue_name][routing_key]
621
- )
622
- if self.rpc_publisher_started and props.reply_to:
623
- self._channel_rpc.basic_publish(
624
- "",
625
- props.reply_to,
626
- "Provider Error: routing_key not binded",
627
- properties=BasicProperties(
628
- correlation_id=props.correlation_id,
629
- content_type="text/plain",
630
- type="error",
631
- ),
632
- )
633
- return not self.auto_ack and self._channel.is_open and self._channel.basic_nack(
634
- basic_deliver.delivery_tag, requeue=False
674
+ handlers = self.subscribes.get(queue_name, {})
675
+ handler = handlers.get(basic_deliver.routing_key)
676
+ if handler:
677
+ return await process_message(handler)
678
+
679
+ queue_type = getattr(self, "_queue_exchange_type", {}).get(queue_name, "topic")
680
+ if queue_type != "direct":
681
+ patterns = getattr(self, "_compiled_patterns", {}).get(queue_name)
682
+ if patterns:
683
+ for pattern_key, pattern in patterns:
684
+ if pattern.match(basic_deliver.routing_key):
685
+ pattern_handler = handlers.get(pattern_key)
686
+ if pattern_handler:
687
+ return await process_message(pattern_handler)
688
+ elif self.re_subscribes:
689
+ for routing_key in self.re_subscribes:
690
+ if routing_key in handlers and re.match(routing_key, basic_deliver.routing_key):
691
+ return await process_message(handlers[routing_key])
692
+
693
+ if self.rpc_publisher_started and props.reply_to:
694
+ self._channel_rpc.basic_publish(
695
+ "",
696
+ props.reply_to,
697
+ "Provider Error: routing_key not binded",
698
+ properties=BasicProperties(
699
+ correlation_id=props.correlation_id,
700
+ content_type="text/plain",
701
+ type="error",
702
+ ),
635
703
  )
704
+ return not self.auto_ack and self._channel.is_open and self._channel.basic_nack(
705
+ basic_deliver.delivery_tag, requeue=False
706
+ )
636
707
 
637
708
  except BaseException as err:
638
709
  if self.rpc_publisher_started and props.reply_to:
@@ -1,5 +1,5 @@
1
1
  from collections import deque
2
- from typing import Optional, Callable, Awaitable, Tuple, Dict, Any
2
+ from typing import Optional, Callable, Awaitable, Tuple, Dict, Any, Union
3
3
 
4
4
  from .async_connection_factory import AsyncConnectionFactoryRabbitMQ, AsyncioConnection
5
5
  from .async_channel import AsyncChannel
@@ -64,7 +64,7 @@ class AsyncConnection:
64
64
  self.reconnect_delay = 1
65
65
  self.callbacks: deque[Tuple[Callable, Future]] = deque()
66
66
  self.type = connection_type
67
- self.backup: Dict[str, Dict[str, Any]] = {
67
+ self.backup: Dict[str, Dict[Union[str, Tuple[str, str]], Any]] = {
68
68
  "exchange": {},
69
69
  "queue": {},
70
70
  "subscribe": {},
@@ -134,28 +134,8 @@ class AsyncConnection:
134
134
  self._channel.publisher_confirms = self.publisher_confirms
135
135
 
136
136
  if self.reconnecting or self.backup["subscribe"] or self.backup["rpc_subscribe"]:
137
- async def recorvery():
138
- for routing_key in list(self.backup["subscribe"].keys()):
139
- params = self.backup["subscribe"][routing_key]
140
- await self.subscribe(
141
- params["queue_name"],
142
- params["exchange_name"],
143
- routing_key,
144
- params["callback"],
145
- params["timeout"],
146
- )
147
- for routing_key in list(self.backup["rpc_subscribe"].keys()):
148
- params = self.backup["rpc_subscribe"][routing_key]
149
- await self.rpc_subscribe(
150
- params["queue_name"],
151
- params["exchange_name"],
152
- routing_key,
153
- params["callback"],
154
- params["timeout"],
155
- )
156
-
157
137
  future: Future = Future(loop=self.ioloop)
158
- self.callbacks.append((recorvery, future))
138
+ self.callbacks.append((self._recover_subscriptions, future))
159
139
  self.reconnect_delay = 1
160
140
  self.reconnecting = False
161
141
 
@@ -207,12 +187,39 @@ class AsyncConnection:
207
187
  self.reconnecting = True
208
188
  self.retry_connection()
209
189
 
190
+ async def _recover_subscriptions(self):
191
+ for key, params in list(self.backup["subscribe"].items()):
192
+ routing_key = params.get("routing_key") or (key[1] if isinstance(key, tuple) else key)
193
+ await self.subscribe(
194
+ params["queue_name"],
195
+ params["exchange_name"],
196
+ routing_key,
197
+ params["callback"],
198
+ params["timeout"],
199
+ content_type=params.get("content_type", "application/json"),
200
+ auto_decode=params.get("auto_decode", True),
201
+ exchange_type=params.get("exchange_type", "topic"),
202
+ durable=params.get("durable", True),
203
+ auto_delete=params.get("auto_delete", False),
204
+ )
205
+ for key, params in list(self.backup["rpc_subscribe"].items()):
206
+ routing_key = params.get("routing_key") or (key[1] if isinstance(key, tuple) else key)
207
+ await self.rpc_subscribe(
208
+ params["queue_name"],
209
+ params["exchange_name"],
210
+ routing_key,
211
+ params["callback"],
212
+ params["timeout"],
213
+ content_type=params.get("content_type", "application/json"),
214
+ auto_decode=params.get("auto_decode", True),
215
+ exchange_type=params.get("exchange_type", "topic"),
216
+ durable=params.get("durable", True),
217
+ auto_delete=params.get("auto_delete", False),
218
+ )
219
+
210
220
  def retry_connection(self):
211
221
  """
212
222
  Attempts to reconnect to RabbitMQ with exponential backoff.
213
-
214
- If reconnection is successful, this method will recover previous subscriptions
215
- and RPC handlers.
216
223
  """
217
224
  if not self.is_open:
218
225
  if self.reconnect_delay > 30:
@@ -224,29 +231,9 @@ class AsyncConnection:
224
231
  self.reconnect_delay += 1
225
232
  else:
226
233
  if self.reconnecting:
227
- async def recorvery():
228
- for routing_key in list(self.backup["subscribe"].keys()):
229
- params = self.backup["subscribe"][routing_key]
230
- await self.subscribe(
231
- params["queue_name"],
232
- params["exchange_name"],
233
- routing_key,
234
- params["callback"],
235
- params["timeout"],
236
- )
237
- for routing_key in list(self.backup["rpc_subscribe"].keys()):
238
- params = self.backup["rpc_subscribe"][routing_key]
239
- await self.rpc_subscribe(
240
- params["queue_name"],
241
- params["exchange_name"],
242
- routing_key,
243
- params["callback"],
244
- params["timeout"],
245
- )
246
-
247
234
  self.reconnect_delay = 1
248
235
  self.reconnecting = False
249
- self.ioloop.create_task(self.add_callback(recorvery)) # type: ignore
236
+ self.ioloop.create_task(self.add_callback(self._recover_subscriptions)) # type: ignore
250
237
 
251
238
  @property
252
239
  def is_open(self) -> Optional[bool]:
@@ -386,7 +373,17 @@ class AsyncConnection:
386
373
  )
387
374
 
388
375
  async def rpc_subscribe(
389
- self, queue_name, exchange_name, routing_key, callback, timeout
376
+ self,
377
+ queue_name,
378
+ exchange_name,
379
+ routing_key,
380
+ callback,
381
+ timeout,
382
+ content_type="application/json",
383
+ auto_decode=True,
384
+ exchange_type="topic",
385
+ durable=True,
386
+ auto_delete=False,
390
387
  ):
391
388
  """
392
389
  Registers an RPC handler for a specific routing key.
@@ -397,24 +394,24 @@ class AsyncConnection:
397
394
  routing_key: The routing key to subscribe to
398
395
  callback: The function to call when a message is received
399
396
  timeout: Timeout in seconds for processing the received message
400
-
401
- Examples:
402
- >>> async def handle_rpc(body):
403
- result = process_request(body)
404
- return json.dumps(result).encode()
405
- >>> await connection.rpc_subscribe(
406
- "rpc_queue",
407
- "rpc_exchange",
408
- "user.find",
409
- handle_rpc,
410
- 10.0
411
- )
397
+ content_type: Content type of expected message
398
+ auto_decode: Whether to auto-decode JSON payload
399
+ exchange_type: Exchange type for RPC server (default 'topic')
400
+ durable: Durability of queue (default True)
401
+ auto_delete: Auto delete flag for queue (default False)
412
402
  """
413
- self.backup["rpc_subscribe"][routing_key] = {
403
+ sub_key = (queue_name, routing_key)
404
+ self.backup["rpc_subscribe"][sub_key] = {
414
405
  "queue_name": queue_name,
415
406
  "exchange_name": exchange_name,
407
+ "routing_key": routing_key,
416
408
  "callback": callback,
417
409
  "timeout": timeout,
410
+ "content_type": content_type,
411
+ "auto_decode": auto_decode,
412
+ "exchange_type": exchange_type,
413
+ "durable": durable,
414
+ "auto_delete": auto_delete,
418
415
  }
419
416
  await self._channel.rpc_subscribe(
420
417
  queue_name=queue_name,
@@ -422,6 +419,11 @@ class AsyncConnection:
422
419
  routing_key=routing_key,
423
420
  callback=callback,
424
421
  timeout=timeout,
422
+ content_type=content_type,
423
+ auto_decode=auto_decode,
424
+ exchange_type=exchange_type,
425
+ durable=durable,
426
+ auto_delete=auto_delete,
425
427
  )
426
428
 
427
429
  async def subscribe(
@@ -431,6 +433,11 @@ class AsyncConnection:
431
433
  routing_key: str,
432
434
  callback: Callable[[Any], Awaitable[None]],
433
435
  timeout: Optional[float],
436
+ content_type: str = "application/json",
437
+ auto_decode: bool = True,
438
+ exchange_type: str = "topic",
439
+ durable: bool = True,
440
+ auto_delete: bool = False,
434
441
  ):
435
442
  """
436
443
  Subscribes to messages with a specific routing key.
@@ -441,23 +448,24 @@ class AsyncConnection:
441
448
  routing_key: The routing key to subscribe to
442
449
  callback: The function to call when a message is received
443
450
  timeout: Timeout in seconds for processing the received message
444
-
445
- Examples:
446
- >>> async def handle_message(body):
447
- print(f"Received: {body}")
448
- >>> await connection.subscribe(
449
- "notifications_queue",
450
- "notifications",
451
- "email.send",
452
- handle_message,
453
- 5.0
454
- )
451
+ content_type: Content type of expected message
452
+ auto_decode: Whether to auto-decode JSON payload
453
+ exchange_type: Exchange type for subscription (default 'topic')
454
+ durable: Durability of queue (default True)
455
+ auto_delete: Auto delete flag for queue (default False)
455
456
  """
456
- self.backup["subscribe"][routing_key] = {
457
+ sub_key = (queue_name, routing_key)
458
+ self.backup["subscribe"][sub_key] = {
457
459
  "queue_name": queue_name,
458
460
  "exchange_name": exchange_name,
461
+ "routing_key": routing_key,
459
462
  "callback": callback,
460
463
  "timeout": timeout,
464
+ "content_type": content_type,
465
+ "auto_decode": auto_decode,
466
+ "exchange_type": exchange_type,
467
+ "durable": durable,
468
+ "auto_delete": auto_delete,
461
469
  }
462
470
  await self._channel.subscribe(
463
471
  exchange_name=exchange_name,
@@ -465,6 +473,11 @@ class AsyncConnection:
465
473
  routing_key=routing_key,
466
474
  callback=callback,
467
475
  timeout=timeout,
476
+ content_type=content_type,
477
+ auto_decode=auto_decode,
478
+ exchange_type=exchange_type,
479
+ durable=durable,
480
+ auto_delete=auto_delete,
468
481
  )
469
482
 
470
483
  async def add_callback(
@@ -1,7 +1,7 @@
1
1
  from typing import Callable, Awaitable, Optional, Union, Any, List
2
2
  from .async_connection import AsyncConnection
3
3
  from ..domain.utils import ConnectionType
4
- from amqp_client_python.domain.models import Config
4
+ from amqp_client_python.domain.models import Config, Options
5
5
  from asyncio import AbstractEventLoop, get_event_loop
6
6
  from pika import DeliveryMode
7
7
  from amqp_client_python.signals import Signal
@@ -212,6 +212,8 @@ class AsyncEventbusRabbitMQ:
212
212
  handler: Callable[[Any], Awaitable[Union[bytes, str]]],
213
213
  timeout: Optional[int] = None,
214
214
  connection_timeout: int = 16,
215
+ content_type: str = "application/json",
216
+ auto_decode: bool = True,
215
217
  ) -> None:
216
218
  """
217
219
  Register a provider to listen on RPC request queue
@@ -221,6 +223,8 @@ class AsyncEventbusRabbitMQ:
221
223
  handler: message handler, it will be called when a message is received
222
224
  timeout: timeout in seconds for waiting for process the received message
223
225
  connection_timeout: timeout for waiting for connection restabilishment
226
+ content_type: Content type of expected message
227
+ auto_decode: Whether to auto-decode JSON payload (default True)
224
228
 
225
229
  Raises:
226
230
  AutoReconnectException: when cannout reconnect on the gived timeout
@@ -233,12 +237,23 @@ class AsyncEventbusRabbitMQ:
233
237
  """
234
238
 
235
239
  async def add_resource():
240
+ extra_kwargs = {}
241
+ if content_type != "application/json":
242
+ extra_kwargs["content_type"] = content_type
243
+ if not auto_decode:
244
+ extra_kwargs["auto_decode"] = auto_decode
245
+ options = getattr(self.config, "options", None)
246
+ if isinstance(options, Options):
247
+ extra_kwargs["exchange_type"] = options.rpc_exchange_type
248
+ extra_kwargs["durable"] = options.rpc_queue_durable
249
+ extra_kwargs["auto_delete"] = options.rpc_queue_auto_delete
236
250
  await self._rpc_server_connection.rpc_subscribe(
237
251
  self.config.options.rpc_queue_name,
238
252
  self.config.options.rpc_exchange_name,
239
253
  name,
240
254
  handler,
241
255
  timeout,
256
+ **extra_kwargs,
242
257
  )
243
258
 
244
259
  if self._rpc_server_connection.open(self.config.url):
@@ -252,6 +267,8 @@ class AsyncEventbusRabbitMQ:
252
267
  handler: Callable[[Any], Awaitable[None]],
253
268
  timeout: Optional[float] = None,
254
269
  connection_timeout: int = 16,
270
+ content_type: str = "application/json",
271
+ auto_decode: bool = True,
255
272
  ) -> None:
256
273
  """
257
274
  Register a provider to listen on queue of bus
@@ -262,6 +279,8 @@ class AsyncEventbusRabbitMQ:
262
279
  handler: message handler, it will be called when a message is received
263
280
  timeout: timeout in seconds for waiting for process the received message
264
281
  connection_timeout: timeout for waiting for connection restabilishment
282
+ content_type: Content type of expected message
283
+ auto_decode: Whether to auto-decode JSON payload (default True)
265
284
 
266
285
  Raises:
267
286
  AutoReconnectException: when cannout reconnect on the gived timeout
@@ -277,12 +296,23 @@ class AsyncEventbusRabbitMQ:
277
296
  """
278
297
 
279
298
  async def add_subscribe():
299
+ extra_kwargs = {}
300
+ if content_type != "application/json":
301
+ extra_kwargs["content_type"] = content_type
302
+ if not auto_decode:
303
+ extra_kwargs["auto_decode"] = auto_decode
304
+ options = getattr(self.config, "options", None)
305
+ if isinstance(options, Options):
306
+ extra_kwargs["exchange_type"] = options.exchange_type
307
+ extra_kwargs["durable"] = options.queue_durable
308
+ extra_kwargs["auto_delete"] = options.queue_auto_delete
280
309
  await self._sub_connection.subscribe(
281
310
  self.config.options.queue_name,
282
311
  exchange_name,
283
312
  routing_key,
284
313
  handler,
285
314
  timeout,
315
+ **extra_kwargs,
286
316
  )
287
317
 
288
318
  if self._sub_connection.open(self.config.url):
@@ -142,6 +142,8 @@ class EventbusWrapperRabbitMQ:
142
142
  handler: Callable[[Any], Awaitable[None]],
143
143
  response_timeout: Optional[int] = None,
144
144
  connection_timeout: int = 16,
145
+ content_type: str = "application/json",
146
+ auto_decode: bool = True,
145
147
  ) -> Future:
146
148
  if self._thread.ident == current_thread().ident:
147
149
  raise BlockingException(
@@ -154,6 +156,8 @@ class EventbusWrapperRabbitMQ:
154
156
  handler,
155
157
  response_timeout,
156
158
  connection_timeout,
159
+ content_type=content_type,
160
+ auto_decode=auto_decode,
157
161
  ),
158
162
  self._loop,
159
163
  )
@@ -165,13 +169,21 @@ class EventbusWrapperRabbitMQ:
165
169
  handler: Callable[[Any], Awaitable[None]],
166
170
  response_timeout: Optional[int] = None,
167
171
  connection_timeout: int = 16,
172
+ content_type: str = "application/json",
173
+ auto_decode: bool = True,
168
174
  ):
169
175
  if self._thread.ident != current_thread().ident:
170
176
  raise ThreadUnsafeException(
171
177
  "Cannot run async call on this thread, try to use sync thread safe methods"
172
178
  )
173
179
  await self._async_eventbus.subscribe(
174
- exchange_name, routing_key, handler, response_timeout, connection_timeout
180
+ exchange_name,
181
+ routing_key,
182
+ handler,
183
+ response_timeout,
184
+ connection_timeout,
185
+ content_type=content_type,
186
+ auto_decode=auto_decode,
175
187
  )
176
188
 
177
189
  def provide_resource(
@@ -180,6 +192,8 @@ class EventbusWrapperRabbitMQ:
180
192
  callback: Callable[[List[Any]], Awaitable[Union[bytes, str]]],
181
193
  response_timeout: Optional[int] = None,
182
194
  connection_timeout: int = 16,
195
+ content_type: str = "application/json",
196
+ auto_decode: bool = True,
183
197
  ) -> Future:
184
198
  if self._thread.ident == current_thread().ident:
185
199
  raise BlockingException(
@@ -187,7 +201,12 @@ class EventbusWrapperRabbitMQ:
187
201
  )
188
202
  return run_coroutine_threadsafe(
189
203
  self._async_eventbus.provide_resource(
190
- name, callback, response_timeout, connection_timeout
204
+ name,
205
+ callback,
206
+ response_timeout,
207
+ connection_timeout,
208
+ content_type=content_type,
209
+ auto_decode=auto_decode,
191
210
  ),
192
211
  self._loop,
193
212
  )
@@ -198,13 +217,20 @@ class EventbusWrapperRabbitMQ:
198
217
  callback: Callable[[List[Any]], Awaitable[Union[bytes, str]]],
199
218
  response_timeout: Optional[int] = None,
200
219
  connection_timeout: int = 16,
220
+ content_type: str = "application/json",
221
+ auto_decode: bool = True,
201
222
  ):
202
223
  if self._thread.ident != current_thread().ident:
203
224
  raise ThreadUnsafeException(
204
225
  "Cannot run async call on this thread, try to use sync thread safe methods"
205
226
  )
206
227
  await self._async_eventbus.provide_resource(
207
- name, callback, response_timeout, connection_timeout
228
+ name,
229
+ callback,
230
+ response_timeout,
231
+ connection_timeout,
232
+ content_type=content_type,
233
+ auto_decode=auto_decode,
208
234
  )
209
235
 
210
236
  def dispose(self):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: amqp-client-python
3
- Version: 0.2.1.dev1
3
+ Version: 0.2.2.dev1
4
4
  Summary: Python AMQP Client Library
5
5
  Project-URL: Homepage, https://github.com/nutes-uepb/amqp-client-python
6
6
  Project-URL: Repository, https://github.com/nutes-uepb/amqp-client-python
@@ -17,7 +17,6 @@ Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Natural Language :: English
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.9
21
20
  Classifier: Programming Language :: Python :: 3.10
22
21
  Classifier: Programming Language :: Python :: 3.11
23
22
  Classifier: Programming Language :: Python :: 3.12
@@ -28,7 +27,7 @@ Classifier: Topic :: Internet
28
27
  Classifier: Topic :: Software Development :: Libraries
29
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
29
  Classifier: Topic :: System :: Networking
31
- Requires-Python: <4.0,>=3.9
30
+ Requires-Python: <4.0,>=3.10
32
31
  Requires-Dist: pika<2.0.0,>=1.3.0
33
32
  Description-Content-Type: text/markdown
34
33
 
@@ -57,16 +56,6 @@ A Python client providing a high-level abstraction for interacting with RabbitMQ
57
56
 
58
57
  **Discord Server**: <a href="https://discord.gg/RkXNeZpNZk" target="_blank">https://discord.gg/RkXNeZpNZk</a>
59
58
 
60
- ### 🦀 Looking for higher performance? Try `amqp-rs`!
61
- If you need higher throughput, thread safety, and built-in compression, check out [**amqp-rs**](https://pypi.org/project/amqp-rs/).
62
-
63
- It is a high-performance Python extension developed in Rust using PyO3 and `tokio`. It shares a similar API and design philosophy with this library, but adds native thread safety, `zstd`/`zlib`/`lz4` compression, robust TLS/SSL support, and graceful shutdowns.
64
-
65
- You can install it via `pip` or `uv`:
66
- ```bash
67
- pip install amqp-rs
68
- ```
69
-
70
59
  ---
71
60
  ### Features:
72
61
  - Automatic creation and management of queues, exchanges and channels;
@@ -80,19 +69,22 @@ pip install amqp-rs
80
69
  ### Table of Compatibility
81
70
  | version | compatible with |
82
71
  | ---- | ---- |
72
+ | 0.2.1 | ~0.2.0 |
83
73
  | 0.2.0 | 0.2.0 |
84
74
  | 0.1.14 | ~0.1.12 |
85
75
 
86
76
  ### Installation
87
77
 
88
- You can install `amqp-client-python` using pip:
78
+ You can install `amqp-client-python` using uv or pip:
89
79
  ```bash
80
+ uv add amqp-client-python
81
+ # or
90
82
  pip install amqp-client-python
91
83
  ```
92
84
 
93
85
  ### Prerequisites
94
86
 
95
- * Python 3.7+
87
+ * Python 3.9+
96
88
  * A running RabbitMQ instance.
97
89
 
98
90
  ### Examples:
@@ -109,15 +101,18 @@ from amqp_client_python import (
109
101
  )
110
102
  config = Config(Options("queue", "rpc_queue", "rpc_exchange"))
111
103
  eventbus = AsyncEventbusRabbitMQ(config)
112
- # publish
104
+ # publish (supports dict/str with auto-JSON, or raw bytes/bytearray without overhead)
105
+ await eventbus.publish("rpc_exchange", "routing.key", {"message": "content"})
106
+ await eventbus.publish("rpc_exchange", "routing.key", b"raw_binary_payload")
113
107
 
114
- eventbus.publish("rpc_exchange", "routing.key", "message_content")
115
- # subscribe
108
+ # subscribe (auto_decode=True delivers parsed JSON dict; auto_decode=False delivers raw bytes)
116
109
  async def subscribe_handler(body) -> None:
117
110
  print(body, type(body), flush=True) # handle messages
118
111
  await eventbus.subscribe("rpc_exchange", "routing.key", subscribe_handler)
112
+
119
113
  # rpc_publish
120
- response = await eventbus.rpc_client("rpc_exchange", "user.find", "message_content")
114
+ response = await eventbus.rpc_client("rpc_exchange", "user.find", {"name": "alice"})
115
+
121
116
  # provider
122
117
  async def rpc_provider_handler(body) -> bytes:
123
118
  print(f"body: {body}")
@@ -3,7 +3,7 @@ amqp_client_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  amqp_client_python/signals.py,sha256=6mW4oWBQnbJwuKox6ceeDbxTM8v9aCDEb0o2zyhpb5g,1263
4
4
  amqp_client_python/domain/models/__init__.py,sha256=EgEV-FeMUVMj_7RvhPbnZaqnJaNqTYnIAiEiqSy0ies,154
5
5
  amqp_client_python/domain/models/config.py,sha256=g2AWrlEvHCS1h2zlny29phREyG9GmD0rtGQHS3C7LVU,2268
6
- amqp_client_python/domain/models/options.py,sha256=lx-x4uyMpyjelZjMuyUxLsORz1ro1vDjD33j0EGYD-o,2141
6
+ amqp_client_python/domain/models/options.py,sha256=uCApGDYfwKUVytoQ-g81mbHoEWvVp5kVObUlYpWBD1c,3376
7
7
  amqp_client_python/domain/models/ssl_options.py,sha256=xOTSIKfbNZbYepY6o3SxuZvLPX3J5pvRoJXIJlyBsrI,613
8
8
  amqp_client_python/domain/utils/__init__.py,sha256=lZL3bwchrTiiKx5d1RSVUJACAuPb7zOnoH9tZ65t_Ok,131
9
9
  amqp_client_python/domain/utils/connection_type.py,sha256=AnEF8Nd3cQBCGX8rPKwEkKts8kwnGUAVr0nxnbpynLE,164
@@ -27,19 +27,19 @@ amqp_client_python/exceptions/rpc_provider_exception.py,sha256=_1Ytb8BOUT_9xEtG_
27
27
  amqp_client_python/exceptions/thread_unsafe_exception.py,sha256=zcC4VyuxXWo66Tts1AQtkg07GK3OjXu3LEtt1AUj4jk,281
28
28
  amqp_client_python/exceptions/timeout_exception.py,sha256=0YT-vivS44mHF4Jc3o2rlZFc6l_zzNjcxCHPS9ArgN0,271
29
29
  amqp_client_python/rabbitmq/__init__.py,sha256=4N-j8rbrJj0kOOSIhSWJenj6JOJbzFNnW3bxwPGTyuM,941
30
- amqp_client_python/rabbitmq/async_channel.py,sha256=7QQTKqqe5uFBr9TqUOS0X0tMEMNJavstRnQ9RHWl2wA,26926
30
+ amqp_client_python/rabbitmq/async_channel.py,sha256=cAis6UoMlmhb1HtMavnb36KC-AkK7c-aVc1GufO6bUs,30495
31
31
  amqp_client_python/rabbitmq/async_channel_factory.py,sha256=ec_5B3Sgazi_ncIG5lI5nnmkH3NprwvyjMhvgpG1BWA,371
32
- amqp_client_python/rabbitmq/async_connection.py,sha256=p05Y_1iXcytROZuaPqmodlKxsdxU50ALhEDjN7WqEhw,18308
32
+ amqp_client_python/rabbitmq/async_connection.py,sha256=kq0jjXsuivmi101gj809ME_-LcrLYbey9rO9k0h-d0g,19063
33
33
  amqp_client_python/rabbitmq/async_connection_factory.py,sha256=3N5MM1fPUdodAFLh58rf6vvwN9OAclDVzmG85Qve-Ss,849
34
- amqp_client_python/rabbitmq/async_eventbus_rabbitmq.py,sha256=4PCtqhVNT3cq-d0anfdKPzhmmETD-HMqIu-MVuZmZZA,12950
34
+ amqp_client_python/rabbitmq/async_eventbus_rabbitmq.py,sha256=mjMtdm6hmony1PXZqOc9molVZ2GRT9wrwRvFRd4AQBs,14553
35
35
  amqp_client_python/rabbitmq/channel_factory_rabbitmq.py,sha256=h_vGqHgL0DOtXC0tmVd5gzXVlNKL48rxpKpwaN3AlOM,310
36
36
  amqp_client_python/rabbitmq/channel_rabbitmq.py,sha256=S3SqGCfVRoIuPyZW40bQZBpWEgbe5YlvZ8ei23a2VVw,13737
37
37
  amqp_client_python/rabbitmq/connection_factory_rabbitmq.py,sha256=EKPG5gEsGwtpSKOhoPI5vK1sJOUARbOflOug6VREIrk,1022
38
38
  amqp_client_python/rabbitmq/connection_rabbitmq.py,sha256=9A8NAPAB3i-5E43SV5sxjKFFoRb4pIhzAAUHrvOvOng,9772
39
39
  amqp_client_python/rabbitmq/eventbus_rabbitmq.py,sha256=GZoJGVeBkMTPrQ5dIJqad-MdGmuuJsxGaXV1YGLdk5o,6980
40
- amqp_client_python/rabbitmq/eventbus_wrapper_rabbitmq.py,sha256=i3C-sZO9EifanfRXReiib56fxxzjV3Cx71rdbJORg78,7442
40
+ amqp_client_python/rabbitmq/eventbus_wrapper_rabbitmq.py,sha256=ee1tWk2_sQ3tRlvItZCqZtqfDLrjn-K4NCQKw7hNiwg,8225
41
41
  amqp_client_python/rabbitmq/ioloop_factory.py,sha256=vfgndKLcazQU0a4elI5RNHnQCTEua--Jjo99bzX8iHo,856
42
- amqp_client_python-0.2.1.dev1.dist-info/METADATA,sha256=1PF9aIMEadkewz66TXxQLkFKeYKU6UtEzrbE6VHHRAw,10155
43
- amqp_client_python-0.2.1.dev1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
44
- amqp_client_python-0.2.1.dev1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
45
- amqp_client_python-0.2.1.dev1.dist-info/RECORD,,
42
+ amqp_client_python-0.2.2.dev1.dist-info/METADATA,sha256=JuldbMrR7B8K4abQT-WwYksmSKoBEY0ApFNYdBhtCd0,9888
43
+ amqp_client_python-0.2.2.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
44
+ amqp_client_python-0.2.2.dev1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
45
+ amqp_client_python-0.2.2.dev1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any