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.
- amqp_client_python/__init__.py +11 -0
- amqp_client_python/domain/models/__init__.py +6 -0
- amqp_client_python/domain/models/config.py +5 -12
- amqp_client_python/domain/models/options.py +1 -5
- amqp_client_python/domain/models/ssl_options.py +1 -6
- amqp_client_python/domain/utils/__init__.py +5 -0
- amqp_client_python/event/__init__.py +9 -0
- amqp_client_python/exceptions/__init__.py +12 -0
- amqp_client_python/rabbitmq/__init__.py +15 -1
- amqp_client_python/rabbitmq/async_channel.py +58 -27
- amqp_client_python/rabbitmq/async_connection.py +287 -41
- amqp_client_python/rabbitmq/async_eventbus_rabbitmq.py +67 -43
- amqp_client_python/rabbitmq/channel_rabbitmq.py +26 -14
- amqp_client_python/rabbitmq/connection_rabbitmq.py +5 -5
- amqp_client_python/rabbitmq/eventbus_rabbitmq.py +4 -4
- {amqp_client_python-0.2.0.dist-info → amqp_client_python-0.2.1.dev1.dist-info}/METADATA +40 -14
- {amqp_client_python-0.2.0.dist-info → amqp_client_python-0.2.1.dev1.dist-info}/RECORD +21 -21
- {amqp_client_python-0.2.0.dist-info → amqp_client_python-0.2.1.dev1.dist-info}/WHEEL +1 -1
- {amqp_client_python-0.2.0.dist-info → amqp_client_python-0.2.1.dev1.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
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:
|
|
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
|
-
"""
|
|
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
|
-
|
|
87
|
-
:
|
|
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
|
-
"""
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
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
|
-
|
|
424
|
+
timeout=timeout,
|
|
226
425
|
)
|
|
227
426
|
|
|
228
427
|
async def subscribe(
|
|
229
|
-
self,
|
|
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
|
-
"
|
|
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
|
-
|
|
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)
|