amqp-client-python 0.2.0__py3-none-any.whl → 0.2.1.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.
@@ -1,4 +1,6 @@
1
- from typing import Optional, Callable, Awaitable, Tuple, Dict, List, Any
1
+ from collections import deque
2
+ from typing import Optional, Callable, Awaitable, Tuple, Dict, Any
3
+
2
4
  from .async_connection_factory import AsyncConnectionFactoryRabbitMQ, AsyncioConnection
3
5
  from .async_channel import AsyncChannel
4
6
  from ..exceptions import AutoReconnectException
@@ -21,6 +23,33 @@ class AsyncConnection:
21
23
  connection_type: Optional[ConnectionType] = None,
22
24
  signal=Signal(),
23
25
  ) -> None:
26
+ """
27
+ Manages asynchronous connections to RabbitMQ with automatic reconnection capabilities.
28
+
29
+ This class handles connection lifecycle, channel management, and provides methods for
30
+ publishing messages, subscribing to queues, and implementing RPC patterns.
31
+
32
+ Args:
33
+ ioloop: The asyncio event loop to use for async operations
34
+ publisher_confirms: Whether to enable publisher confirmations for this connection
35
+ prefetch_count: Maximum number of unacknowledged messages to prefetch
36
+ auto_ack: Whether to automatically acknowledge messages
37
+ connection_type: Type of connection (PUBLISH, SUBSCRIBE, RPC_CLIENT, RPC_SERVER)
38
+ signal: Signal object for event handling
39
+
40
+ Returns:
41
+ None: None
42
+
43
+ Examples:
44
+ >>> connection = AsyncConnection(
45
+ get_event_loop(),
46
+ publisher_confirms=True,
47
+ prefetch_count=10,
48
+ auto_ack=False,
49
+ connection_type=ConnectionType.PUBLISH
50
+ )
51
+ >>> connection.open("amqp://guest:guest@localhost:5672/%2F")
52
+ """
24
53
  self.ioloop = ioloop
25
54
  self.publisher_confirms = publisher_confirms
26
55
  self.connection_factory = AsyncConnectionFactoryRabbitMQ()
@@ -33,7 +62,7 @@ class AsyncConnection:
33
62
  self.openning = False
34
63
  self.reconnecting = False
35
64
  self.reconnect_delay = 1
36
- self.callbacks: List[Tuple[Callable, Future]] = []
65
+ self.callbacks: deque[Tuple[Callable, Future]] = deque()
37
66
  self.type = connection_type
38
67
  self.backup: Dict[str, Dict[str, Any]] = {
39
68
  "exchange": {},
@@ -43,11 +72,24 @@ class AsyncConnection:
43
72
  }
44
73
 
45
74
  def open(self, uri):
75
+ """
76
+ Opens a connection to RabbitMQ using the provided URI.
77
+
78
+ If the connection is not already open and not in the process of opening,
79
+ this method will initiate a new connection.
80
+
81
+ Args:
82
+ uri: The AMQP URI to connect to (e.g., "amqp://guest:guest@localhost:5672/")
83
+
84
+ Examples:
85
+ >>> connection.open("amqp://guest:guest@localhost:5672/")
86
+ """
46
87
  self.url = uri
47
88
  if not self.is_open and not self.openning:
48
89
  if not self.ioloop:
49
90
  self.ioloop = get_event_loop()
50
91
  self.openning = True
92
+ self._closing = False
51
93
  self._connection = self.connection_factory.create_connection(
52
94
  uri=uri,
53
95
  on_connection_open=self.on_connection_open,
@@ -55,13 +97,31 @@ class AsyncConnection:
55
97
  on_connection_closed=self.on_connection_closed,
56
98
  custum_ioloop=self.ioloop,
57
99
  )
100
+ return True
101
+ return False
58
102
 
59
103
  async def close(self):
104
+ """
105
+ Closes the connection to RabbitMQ gracefully.
106
+
107
+ This method will only close the connection if it is currently open.
108
+
109
+ Examples:
110
+ >>> await connection.close()
111
+ """
60
112
  if self.is_open:
61
113
  self._closing = True
62
114
  self._connection.close()
63
115
 
64
116
  def on_connection_open(self, _unused_connection):
117
+ """
118
+ Callback invoked when the connection to RabbitMQ is established.
119
+
120
+ This method creates a channel and emits a connection event.
121
+
122
+ Args:
123
+ _unused_connection: The connection object (not used)
124
+ """
65
125
  LOGGER.info(f"connection openned {_unused_connection}, {self._connection}")
66
126
  self.signal.emmit(Event.CONNECTED, condiction=self.type, loop=self.ioloop)
67
127
  self.openning = False
@@ -72,20 +132,59 @@ class AsyncConnection:
72
132
  signal=self.signal,
73
133
  )
74
134
  self._channel.publisher_confirms = self.publisher_confirms
135
+
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
+ future: Future = Future(loop=self.ioloop)
158
+ self.callbacks.append((recorvery, future))
159
+ self.reconnect_delay = 1
160
+ self.reconnecting = False
161
+
75
162
  self._channel.open(self._connection, self.callbacks)
76
163
 
77
164
  def on_connection_open_error(self, _unused_connection, err):
165
+ """
166
+ Callback invoked when the connection to RabbitMQ is established.
167
+
168
+ This method creates a channel and emits a connection event.
169
+
170
+ Args:
171
+ _unused_connection: The connection object (not used)
172
+ """
78
173
  LOGGER.info(f"connection open error: {err}, will attempt a connection")
79
174
  self.openning = False
80
175
  self.reconnect()
81
176
 
82
177
  def on_connection_closed(self, _unused_connection, reason):
83
- """This method is invoked by pika when the connection to RabbitMQ is
178
+ """
179
+ Callback invoked when the connection to RabbitMQ is closed unexpectedly.
180
+
181
+ This method is invoked by pika when the connection to RabbitMQ is
84
182
  closed unexpectedly. Since it is unexpected, we will reconnect to
85
183
  RabbitMQ if it disconnects.
86
- :param pika.connection.Connection connection: The closed connection obj
87
- :param Exception reason: exception representing reason for loss of
88
- connection.
184
+
185
+ Args:
186
+ _unused_connection: The closed connection object
187
+ reason: Exception representing reason for loss of connection
89
188
  """
90
189
  self._channel = None
91
190
  if self._closing:
@@ -98,15 +197,23 @@ class AsyncConnection:
98
197
  self.reconnect()
99
198
 
100
199
  def reconnect(self):
101
- """Will be invoked if the connection can't be opened or is
102
- closed. Indicates that a reconnect is necessary then stops the
103
- ioloop.
200
+ """
201
+ Initiates the reconnection process.
202
+
203
+ Will be invoked if the connection can't be opened or is closed.
204
+ Indicates that a reconnect is necessary.
104
205
  """
105
206
  if not self.reconnecting:
106
207
  self.reconnecting = True
107
208
  self.retry_connection()
108
209
 
109
210
  def retry_connection(self):
211
+ """
212
+ 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
+ """
110
217
  if not self.is_open:
111
218
  if self.reconnect_delay > 30:
112
219
  self.reconnect_delay = 30
@@ -116,33 +223,39 @@ class AsyncConnection:
116
223
  )
117
224
  self.reconnect_delay += 1
118
225
  else:
226
+ 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
+ )
119
246
 
120
- async def recorvery():
121
- for routing_key in self.backup["subscribe"]:
122
- params = self.backup["subscribe"][routing_key]
123
- await self.subscribe(
124
- params["queue_name"],
125
- params["exchange_name"],
126
- routing_key,
127
- params["callback"],
128
- params["response_timeout"],
129
- )
130
- for routing_key in self.backup["rpc_subscribe"]:
131
- params = self.backup["rpc_subscribe"][routing_key]
132
- await self.rpc_subscribe(
133
- params["queue_name"],
134
- params["exchange_name"],
135
- routing_key,
136
- params["callback"],
137
- params["response_timeout"],
138
- )
139
-
140
- self.ioloop.create_task(self.add_callback(recorvery)) # type: ignore
141
- self.reconnect_delay = 1
142
- self.reconnecting = False
247
+ self.reconnect_delay = 1
248
+ self.reconnecting = False
249
+ self.ioloop.create_task(self.add_callback(recorvery)) # type: ignore
143
250
 
144
251
  @property
145
252
  def is_open(self) -> Optional[bool]:
253
+ """
254
+ Checks if the connection is currently open.
255
+
256
+ Returns:
257
+ Optional[bool]: True if the connection is open, False otherwise, or None if no connection exists
258
+ """
146
259
  return self._connection and self._connection.is_open
147
260
 
148
261
  def stop(self) -> None:
@@ -170,17 +283,49 @@ class AsyncConnection:
170
283
  routing_key: str,
171
284
  body: Any,
172
285
  content_type: str,
173
- timeout,
286
+ response_timeout,
174
287
  delivery_mode,
175
288
  expiration,
176
289
  **kwargs,
177
290
  ):
291
+ """
292
+ Sends an RPC request and waits for a response.
293
+
294
+ Args:
295
+ exchange_name: The exchange to publish to
296
+ routing_key: The routing key for the message
297
+ body: The message body to send
298
+ content_type: Content type of the message
299
+ response_timeout: Timeout in seconds for waiting for a response
300
+ delivery_mode: Delivery mode (persistent or transient)
301
+ expiration: Maximum lifetime of the message in the queue
302
+ **kwargs: Additional message properties
303
+
304
+ Returns:
305
+ Any: The response from the RPC server
306
+
307
+ Raises:
308
+ PublishTimeoutException: If publisher confirmation times out
309
+ NackException: If the message is rejected by the broker
310
+ ResponseTimeoutException: If no response is received within the response_timeout
311
+
312
+ Examples:
313
+ >>> response = await connection.rpc_client(
314
+ "rpc_exchange",
315
+ "user.find",
316
+ {"id": 123},
317
+ "application/json",
318
+ 5.0,
319
+ DeliveryMode.Transient,
320
+ "60000"
321
+ )
322
+ """
178
323
  return await self._channel.rpc_client(
179
324
  exchange_name,
180
325
  routing_key,
181
326
  body,
182
327
  content_type,
183
- timeout,
328
+ response_timeout,
184
329
  delivery_mode,
185
330
  expiration,
186
331
  **kwargs,
@@ -197,6 +342,38 @@ class AsyncConnection:
197
342
  expiration,
198
343
  **kwargs,
199
344
  ):
345
+ """
346
+ Publishes a message to RabbitMQ.
347
+
348
+ Args:
349
+ exchange_name: The exchange to publish to
350
+ routing_key: The routing key for the message
351
+ body: The message body to send
352
+ content_type: Content type of the message
353
+ timeout: Timeout in seconds for publisher confirmation
354
+ delivery_mode: Delivery mode (persistent or transient)
355
+ expiration: Maximum lifetime of the message in the queue
356
+ **kwargs: Additional message properties
357
+
358
+ Returns:
359
+ Optional[bool]: True if publisher confirms enabled and message was confirmed,
360
+ None if publisher confirms disabled
361
+
362
+ Raises:
363
+ PublishTimeoutException: If publisher confirmation times out
364
+ NackException: If the message is rejected by the broker
365
+
366
+ Examples:
367
+ >>> result = await connection.publish(
368
+ "notifications",
369
+ "email.send",
370
+ {"to": "user@example.com", "subject": "Hello"},
371
+ "application/json",
372
+ 5.0,
373
+ DeliveryMode.Persistent,
374
+ "60000"
375
+ )
376
+ """
200
377
  return await self._channel.publish(
201
378
  exchange_name,
202
379
  routing_key,
@@ -209,37 +386,85 @@ class AsyncConnection:
209
386
  )
210
387
 
211
388
  async def rpc_subscribe(
212
- self, queue_name, exchange_name, routing_key, callback, response_timeout
389
+ self, queue_name, exchange_name, routing_key, callback, timeout
213
390
  ):
391
+ """
392
+ Registers an RPC handler for a specific routing key.
393
+
394
+ Args:
395
+ queue_name: The queue to consume from
396
+ exchange_name: The exchange to bind to
397
+ routing_key: The routing key to subscribe to
398
+ callback: The function to call when a message is received
399
+ 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
+ )
412
+ """
214
413
  self.backup["rpc_subscribe"][routing_key] = {
215
414
  "queue_name": queue_name,
216
415
  "exchange_name": exchange_name,
217
416
  "callback": callback,
218
- "response_timeout": response_timeout,
417
+ "timeout": timeout,
219
418
  }
220
419
  await self._channel.rpc_subscribe(
221
420
  queue_name=queue_name,
222
421
  exchange_name=exchange_name,
223
422
  routing_key=routing_key,
224
423
  callback=callback,
225
- response_timeout=response_timeout,
424
+ timeout=timeout,
226
425
  )
227
426
 
228
427
  async def subscribe(
229
- self, queue_name, exchange_name, routing_key, callback, response_timeout
428
+ self,
429
+ queue_name: str,
430
+ exchange_name: str,
431
+ routing_key: str,
432
+ callback: Callable[[Any], Awaitable[None]],
433
+ timeout: Optional[float],
230
434
  ):
435
+ """
436
+ Subscribes to messages with a specific routing key.
437
+
438
+ Args:
439
+ queue_name: The queue to consume from
440
+ exchange_name: The exchange to bind to
441
+ routing_key: The routing key to subscribe to
442
+ callback: The function to call when a message is received
443
+ 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
+ )
455
+ """
231
456
  self.backup["subscribe"][routing_key] = {
232
457
  "queue_name": queue_name,
233
458
  "exchange_name": exchange_name,
234
459
  "callback": callback,
235
- "response_timeout": response_timeout,
460
+ "timeout": timeout,
236
461
  }
237
462
  await self._channel.subscribe(
238
463
  exchange_name=exchange_name,
239
464
  queue_name=queue_name,
240
465
  routing_key=routing_key,
241
466
  callback=callback,
242
- response_timeout=response_timeout,
467
+ timeout=timeout,
243
468
  )
244
469
 
245
470
  async def add_callback(
@@ -247,8 +472,29 @@ class AsyncConnection:
247
472
  callback: Callable[..., Awaitable[Any]],
248
473
  connection_timeout: Optional[float] = None,
249
474
  ):
475
+ """
476
+ Executes a callback when the connection is ready or queues it for later execution.
477
+
478
+ If the connection and channel are open, executes the callback immediately.
479
+ Otherwise, queues the callback to be executed when the connection is established.
480
+
481
+ Args:
482
+ callback: The async function to call
483
+ connection_timeout: Maximum time to wait for the connection to be established
484
+
485
+ Returns:
486
+ Any: The result of the callback
487
+
488
+ Raises:
489
+ AutoReconnectException: When the connection cannot be established within the timeout
490
+
491
+ Examples:
492
+ >>> async def my_operation():
493
+ return await connection.publish(...)
494
+ >>> result = await connection.add_callback(my_operation, 10.0)
495
+ """
250
496
  try:
251
- if self.is_open and self._channel.is_open:
497
+ if self.is_open and self._channel and self._channel.is_open:
252
498
  return await callback()
253
499
  else:
254
500
  future: Future = Future(loop=self.ioloop)