DefStream-RabbitMQ 0.1.5__16-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.

Potentially problematic release.


This version of DefStream-RabbitMQ might be problematic. Click here for more details.

@@ -0,0 +1,278 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Callable,
6
+ RemoteError,
7
+ validate_priority,
8
+ )
9
+
10
+ from .Serializer import (
11
+ Encoder,
12
+ Decoder,
13
+ )
14
+ from copy import deepcopy
15
+ from logging import getLogger
16
+ from math import (
17
+ ceil,
18
+ isfinite,
19
+ )
20
+
21
+ from pika import (
22
+ BlockingConnection,
23
+ BasicProperties,
24
+ ConnectionParameters,
25
+ )
26
+
27
+ from time import (
28
+ time,
29
+ monotonic,
30
+ )
31
+ from enum import Enum
32
+ from uuid import uuid4
33
+
34
+ from typing import (
35
+ Any,
36
+ Dict,
37
+ Optional,
38
+ )
39
+
40
+ __all__ = (
41
+ 'Exchange',
42
+ )
43
+
44
+
45
+ class Exchange(
46
+ Sendable,
47
+ Callable,
48
+ ):
49
+ """Exchange of Event Broker for RabbitMQ"""
50
+ logger = getLogger('DefStream.Broker.Implements.RabbitMQ.Exchange')
51
+
52
+ def __init__(
53
+ self,
54
+ connection: BlockingConnection,
55
+ encode: Encoder,
56
+ name: str = None,
57
+ decode: Optional[Decoder] = None,
58
+ rpc_parameters: Optional[ConnectionParameters] = None,
59
+ ):
60
+ self.connection = connection
61
+ self.encode = encode
62
+ try:
63
+ self.channel = self.connection.channel()
64
+ except Exception as error:
65
+ self.logger.error('Exchange channel creation failed: exchange=%s error_type=%s', name, type(error).__name__)
66
+ raise
67
+ self.channel.auto_decode = False
68
+ self.exchange = name
69
+ self.decode = decode
70
+ self.rpc_parameters = rpc_parameters
71
+ return
72
+
73
+ def __del__(self):
74
+ channel = getattr(self, 'channel', None)
75
+ try:
76
+ if channel and channel.is_open:
77
+ channel.close()
78
+ self.logger.debug('Exchange channel cleanup completed: exchange=%s', self.exchange)
79
+ except Exception as error:
80
+ self.logger.error('Exchange channel cleanup failed: error_type=%s', type(error).__name__)
81
+ raise
82
+ return
83
+
84
+ def __str__(self): return self.exchange
85
+
86
+ def send(
87
+ self,
88
+ body,
89
+ event: str = None,
90
+ headers: Dict = {},
91
+ priority: Optional[int] = None,
92
+ expiration: int = None,
93
+ timestamp: int = None,
94
+ persistent: bool = True,
95
+ id: str = None,
96
+ ):
97
+ validate_priority(
98
+ priority,
99
+ 255,
100
+ )
101
+ if not timestamp: timestamp = int(time())
102
+ if not id: id = uuid4().hex
103
+ properties = BasicProperties(
104
+ type=event if event else '',
105
+ headers=headers,
106
+ content_type=self.encode.format,
107
+ content_encoding=self.encode.charset,
108
+ priority=priority,
109
+ timestamp=timestamp,
110
+ expiration=expiration,
111
+ message_id=id,
112
+ delivery_mode=2 if persistent else 1,
113
+ )
114
+ self.logger.debug('Publishing message: exchange=%s routing_key=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.exchange, event, id, priority, persistent)
115
+ try:
116
+ self.channel.basic_publish(
117
+ exchange=self.exchange,
118
+ routing_key=event if event else '',
119
+ properties=properties,
120
+ body=self.encode(body),
121
+ )
122
+ except Exception as error:
123
+ self.logger.error('Message publication failed: exchange=%s routing_key=%s message_id=%s error_type=%s', self.exchange, event, id, type(error).__name__)
124
+ raise
125
+ self.logger.debug('Publish API call completed: exchange=%s message_id=%s does not imply Consumer processing completion', self.exchange, id)
126
+ return id
127
+
128
+ def call(
129
+ self,
130
+ body: Any,
131
+ timeout: float = 5,
132
+ *,
133
+ event: Optional[str] = None,
134
+ ) -> Any:
135
+ if self.rpc_parameters is None or self.decode is None:
136
+ raise RuntimeError('RPC requires decoder and connection parameters; obtain the exchange through Connection.exchange')
137
+ if isinstance(
138
+ timeout,
139
+ bool,
140
+ ) or not isinstance(
141
+ timeout,
142
+ (
143
+ int,
144
+ float,
145
+ ),
146
+ ) or not isfinite(timeout) or timeout <= 0:
147
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
148
+ deadline = monotonic() + timeout
149
+ settings = deepcopy(self.rpc_parameters)
150
+ settings.connection_attempts = 1
151
+ settings.socket_timeout = min(
152
+ settings.socket_timeout,
153
+ timeout,
154
+ )
155
+ settings.stack_timeout = timeout
156
+ settings.blocked_connection_timeout = timeout
157
+ timed_out = False
158
+ self.logger.debug('Opening dedicated RPC connection: exchange=%s routing_key=%s timeout=%s', self.exchange, event, timeout)
159
+ try:
160
+ connection = BlockingConnection(settings)
161
+ except Exception as error:
162
+ self.logger.error('Dedicated RPC connection failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
163
+ raise
164
+ try:
165
+ channel = connection.channel()
166
+ reply_queue = channel.queue_declare(
167
+ queue='',
168
+ exclusive=True,
169
+ auto_delete=True,
170
+ ).method.queue
171
+ channel.confirm_delivery()
172
+ correlation_id = uuid4().hex
173
+ self.logger.debug('RPC reply queue created: exchange=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.exchange, reply_queue, correlation_id)
174
+ responses = []
175
+
176
+ def receive(
177
+ channel,
178
+ method,
179
+ properties,
180
+ payload,
181
+ ):
182
+ if properties.correlation_id == correlation_id:
183
+ responses.append((
184
+ properties,
185
+ payload,
186
+ ))
187
+ self.logger.debug('RPC response received: exchange=%s correlation_id=%s', self.exchange, correlation_id)
188
+ else:
189
+ self.logger.debug('Ignoring RPC response with mismatched correlation_id: exchange=%s correlation_id=%s', self.exchange, properties.correlation_id)
190
+
191
+ channel.basic_consume(
192
+ reply_queue,
193
+ on_message_callback=receive,
194
+ auto_ack=True,
195
+ )
196
+ remaining = deadline - monotonic()
197
+ if remaining <= 0:
198
+ timed_out = True
199
+ self.logger.warning('RPC timed out before publication: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
200
+ raise TimeoutError('RPC timed out before request publication')
201
+ self.logger.debug('Publishing RPC request: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
202
+ channel.basic_publish(
203
+ exchange=self.exchange,
204
+ routing_key=event if event is not None else '',
205
+ body=self.encode(body),
206
+ properties=BasicProperties(
207
+ reply_to=reply_queue,
208
+ correlation_id=correlation_id,
209
+ content_type=self.encode.format,
210
+ content_encoding=self.encode.charset,
211
+ expiration=str(max(
212
+ 1,
213
+ ceil(remaining * 1000),
214
+ )),
215
+ ),
216
+ mandatory=True,
217
+ )
218
+ self.logger.debug('RPC request published: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
219
+ while not responses:
220
+ remaining = deadline - monotonic()
221
+ if remaining <= 0:
222
+ timed_out = True
223
+ self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
224
+ raise TimeoutError(f'RPC response timed out for exchange {self.exchange!r} routing_key={event!r}')
225
+ connection.process_data_events(time_limit=min(
226
+ remaining,
227
+ 0.1,
228
+ ))
229
+ properties, payload = responses[0]
230
+ response = self.decode(
231
+ payload,
232
+ properties.content_type,
233
+ properties.content_encoding,
234
+ )
235
+ if not isinstance(
236
+ response,
237
+ dict,
238
+ ) or type(response.get('ok')) is not bool:
239
+ raise ValueError('Invalid RPC response envelope')
240
+ if response['ok']:
241
+ if 'result' not in response:
242
+ raise ValueError('RPC response has no result')
243
+ self.logger.debug('RPC response validated: exchange=%s routing_key=%s correlation_id=%s elapsed=%s', self.exchange, event, correlation_id, timeout - (deadline - monotonic()))
244
+ return response['result']
245
+ error = response.get('error')
246
+ if not isinstance(
247
+ error,
248
+ dict,
249
+ ) or not isinstance(
250
+ error.get('code'),
251
+ str,
252
+ ) or not isinstance(
253
+ error.get('message'),
254
+ str,
255
+ ):
256
+ raise ValueError('Invalid RPC error response')
257
+ self.logger.warning('RPC remote task error: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
258
+ raise RemoteError(
259
+ error['code'],
260
+ error['message'],
261
+ )
262
+ except TimeoutError:
263
+ if not timed_out:
264
+ self.logger.error('RPC internal timeout error: exchange=%s routing_key=%s', self.exchange, event)
265
+ raise
266
+ except RemoteError:
267
+ raise
268
+ except Exception as error:
269
+ self.logger.error('RPC transmission or response handling failed: exchange=%s routing_key=%s error_type=%s', self.exchange, event, type(error).__name__)
270
+ raise
271
+ finally:
272
+ try:
273
+ if connection.is_open:
274
+ connection.close()
275
+ self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: exchange=%s', self.exchange)
276
+ except Exception as error:
277
+ self.logger.error('Dedicated RPC connection cleanup failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
278
+ raise
@@ -0,0 +1,9 @@
1
+ from DefStream.Broker import (
2
+ Requeue,
3
+ Reject,
4
+ )
5
+
6
+ __all__ = (
7
+ 'Requeue',
8
+ 'Reject',
9
+ )
@@ -0,0 +1,319 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Callable,
6
+ Poppable,
7
+ RemoteError,
8
+ validate_priority,
9
+ )
10
+
11
+ from .Serializer import (
12
+ Encoder,
13
+ Decoder,
14
+ )
15
+ from .Event import Event
16
+ from copy import deepcopy
17
+ from logging import getLogger
18
+ from math import (
19
+ ceil,
20
+ isfinite,
21
+ )
22
+
23
+ from pika import (
24
+ BlockingConnection,
25
+ BasicProperties,
26
+ ConnectionParameters,
27
+ )
28
+
29
+ from time import (
30
+ time,
31
+ sleep,
32
+ monotonic,
33
+ )
34
+ from uuid import uuid4
35
+
36
+ from typing import (
37
+ Any,
38
+ Optional,
39
+ Dict,
40
+ )
41
+
42
+ __all__ = (
43
+ 'Queue'
44
+ )
45
+
46
+
47
+ class Queue(
48
+ Sendable,
49
+ Callable,
50
+ Poppable,
51
+ ):
52
+ """Queue of Event Broker for RabbitMQ"""
53
+ logger = getLogger('DefStream.Broker.Implements.RabbitMQ.Queue')
54
+
55
+ def __init__(
56
+ self,
57
+ connection: BlockingConnection,
58
+ encode: Encoder,
59
+ decode: Decoder,
60
+ name: str = None,
61
+ rpc_parameters: Optional[ConnectionParameters] = None,
62
+ ):
63
+ self.connection = connection
64
+ self.encode = encode
65
+ self.decode = decode
66
+ try:
67
+ self.channel = self.connection.channel()
68
+ except Exception as error:
69
+ self.logger.error('Queue channel creation failed: queue=%s error_type=%s', name, type(error).__name__)
70
+ raise
71
+ self.channel.auto_decode = False
72
+ self.queue = name
73
+ self.event = None
74
+ self.rpc_parameters = rpc_parameters
75
+ return
76
+
77
+ def __del__(self):
78
+ channel = getattr(self, 'channel', None)
79
+ try:
80
+ if channel and channel.is_open:
81
+ channel.close()
82
+ self.logger.debug('Queue channel cleanup completed: queue=%s', self.queue)
83
+ except Exception as error:
84
+ self.logger.error('Queue channel cleanup failed: error_type=%s', type(error).__name__)
85
+ raise
86
+ return
87
+
88
+ def __str__(self): return self.queue
89
+
90
+ def send(
91
+ self,
92
+ body,
93
+ event: str = None,
94
+ headers: Dict = {},
95
+ priority: Optional[int] = None,
96
+ expiration: int = None,
97
+ timestamp: int = None,
98
+ persistent: bool = True,
99
+ id: str = None,
100
+ reply_to: str = None,
101
+ correlation_id: str = None,
102
+ ):
103
+ validate_priority(
104
+ priority,
105
+ 255,
106
+ )
107
+ if not timestamp: timestamp = int(time())
108
+ if not id: id = uuid4().hex
109
+ properties = BasicProperties(
110
+ type=event if event else '',
111
+ headers=headers,
112
+ content_type=self.encode.format,
113
+ content_encoding=self.encode.charset,
114
+ priority=priority,
115
+ timestamp=timestamp,
116
+ expiration=expiration,
117
+ message_id=id,
118
+ delivery_mode=2 if persistent else 1,
119
+ reply_to=reply_to,
120
+ correlation_id=correlation_id,
121
+ )
122
+ self.logger.debug('Publishing message: queue=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.queue, id, priority, persistent)
123
+ try:
124
+ self.channel.basic_publish(
125
+ exchange='',
126
+ routing_key=self.queue,
127
+ properties=properties,
128
+ body=self.encode(body)
129
+ )
130
+ except Exception as error:
131
+ self.logger.error('Message publication failed: queue=%s message_id=%s error_type=%s', self.queue, id, type(error).__name__)
132
+ raise
133
+ self.logger.debug('Publish API call completed: queue=%s message_id=%s does not imply Consumer processing completion', self.queue, id)
134
+ return id
135
+
136
+ def call(
137
+ self,
138
+ body: Any,
139
+ timeout: float = 5,
140
+ *,
141
+ priority: Optional[int] = None,
142
+ ) -> Any:
143
+ validate_priority(
144
+ priority,
145
+ 255,
146
+ )
147
+ if self.rpc_parameters is None:
148
+ raise RuntimeError('RPC requires connection parameters; obtain the queue through Connection.queue')
149
+ if isinstance(
150
+ timeout,
151
+ bool,
152
+ ) or not isinstance(
153
+ timeout,
154
+ (
155
+ int,
156
+ float,
157
+ ),
158
+ ) or not isfinite(timeout) or timeout <= 0:
159
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
160
+ deadline = monotonic() + timeout
161
+ settings = deepcopy(self.rpc_parameters)
162
+ settings.connection_attempts = 1
163
+ settings.socket_timeout = min(
164
+ settings.socket_timeout,
165
+ timeout,
166
+ )
167
+ settings.stack_timeout = timeout
168
+ settings.blocked_connection_timeout = timeout
169
+ timed_out = False
170
+ self.logger.debug('Opening dedicated RPC connection: queue=%s timeout=%s', self.queue, timeout)
171
+ try:
172
+ connection = BlockingConnection(settings)
173
+ except Exception as error:
174
+ self.logger.error('Dedicated RPC connection failed: queue=%s error_type=%s', self.queue, type(error).__name__)
175
+ raise
176
+ try:
177
+ channel = connection.channel()
178
+ reply_queue = channel.queue_declare(
179
+ queue='',
180
+ exclusive=True,
181
+ auto_delete=True,
182
+ ).method.queue
183
+ channel.confirm_delivery()
184
+ correlation_id = uuid4().hex
185
+ self.logger.debug('RPC reply queue created: queue=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.queue, reply_queue, correlation_id)
186
+ responses = []
187
+
188
+ def receive(
189
+ channel,
190
+ method,
191
+ properties,
192
+ payload,
193
+ ):
194
+ if properties.correlation_id == correlation_id:
195
+ responses.append((
196
+ properties,
197
+ payload,
198
+ ))
199
+ self.logger.debug('RPC response received: queue=%s correlation_id=%s', self.queue, correlation_id)
200
+ else:
201
+ self.logger.debug('Ignoring RPC response with mismatched correlation_id: queue=%s correlation_id=%s', self.queue, properties.correlation_id)
202
+
203
+ channel.basic_consume(
204
+ reply_queue,
205
+ on_message_callback=receive,
206
+ auto_ack=True,
207
+ )
208
+ remaining = deadline - monotonic()
209
+ if remaining <= 0:
210
+ timed_out = True
211
+ self.logger.warning('RPC timed out before publication: queue=%s correlation_id=%s', self.queue, correlation_id)
212
+ raise TimeoutError('RPC timed out before request publication')
213
+ self.logger.debug('Publishing RPC request: queue=%s correlation_id=%s priority=%s', self.queue, correlation_id, priority)
214
+ channel.basic_publish(
215
+ exchange='',
216
+ routing_key=self.queue,
217
+ body=self.encode(body),
218
+ properties=BasicProperties(
219
+ priority=priority,
220
+ reply_to=reply_queue,
221
+ correlation_id=correlation_id,
222
+ content_type=self.encode.format,
223
+ content_encoding=self.encode.charset,
224
+ expiration=str(max(
225
+ 1,
226
+ ceil(remaining * 1000),
227
+ )),
228
+ ),
229
+ mandatory=True,
230
+ )
231
+ self.logger.debug('RPC request published: queue=%s correlation_id=%s', self.queue, correlation_id)
232
+ while not responses:
233
+ remaining = deadline - monotonic()
234
+ if remaining <= 0:
235
+ timed_out = True
236
+ self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: queue=%s correlation_id=%s', self.queue, correlation_id)
237
+ raise TimeoutError(f'RPC response timed out for queue {self.queue!r}')
238
+ connection.process_data_events(time_limit=min(
239
+ remaining,
240
+ 0.1,
241
+ ))
242
+ properties, payload = responses[0]
243
+ response = self.decode(
244
+ payload,
245
+ properties.content_type,
246
+ properties.content_encoding,
247
+ )
248
+ if not isinstance(
249
+ response,
250
+ dict,
251
+ ) or type(response.get('ok')) is not bool:
252
+ raise ValueError('Invalid RPC response envelope')
253
+ if response['ok']:
254
+ if 'result' not in response:
255
+ raise ValueError('RPC response has no result')
256
+ self.logger.debug('RPC response validated: queue=%s correlation_id=%s elapsed=%s', self.queue, correlation_id, timeout - (deadline - monotonic()))
257
+ return response['result']
258
+ error = response.get('error')
259
+ if not isinstance(
260
+ error,
261
+ dict,
262
+ ) or not isinstance(
263
+ error.get('code'),
264
+ str,
265
+ ) or not isinstance(
266
+ error.get('message'),
267
+ str,
268
+ ):
269
+ raise ValueError('Invalid RPC error response')
270
+ self.logger.warning('RPC remote task error: queue=%s correlation_id=%s', self.queue, correlation_id)
271
+ raise RemoteError(
272
+ error['code'],
273
+ error['message'],
274
+ )
275
+ except TimeoutError:
276
+ if not timed_out:
277
+ self.logger.error('RPC internal timeout error: queue=%s', self.queue)
278
+ raise
279
+ except RemoteError:
280
+ raise
281
+ except Exception as error:
282
+ self.logger.error('RPC transmission or response handling failed: queue=%s error_type=%s', self.queue, type(error).__name__)
283
+ raise
284
+ finally:
285
+ try:
286
+ if connection.is_open:
287
+ connection.close()
288
+ self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: queue=%s', self.queue)
289
+ except Exception as error:
290
+ self.logger.error('Dedicated RPC connection cleanup failed: queue=%s error_type=%s', self.queue, type(error).__name__)
291
+ raise
292
+
293
+ def pop(self, timeout: int = None) -> Optional[Event]:
294
+ self.logger.debug('Waiting for a single message: queue=%s timeout_ms=%s', self.queue, timeout)
295
+ channel = self.channel
296
+ try:
297
+ channel.basic_qos(0, 1, False)
298
+ deadline = time() + (timeout / 1000) if timeout else None
299
+ while True:
300
+ method, properties, body = channel.basic_get(queue=self.queue, auto_ack=False)
301
+ if method:
302
+ event = Event(
303
+ channel,
304
+ self.queue,
305
+ None,
306
+ method.delivery_tag,
307
+ properties,
308
+ self.decode(body, properties.content_type, properties.content_encoding),
309
+ )
310
+ self.logger.debug('Single message received and decoded: queue=%s delivery_tag=%s message_id=%s', self.queue, method.delivery_tag, properties.message_id)
311
+ return event
312
+ if deadline and time() >= deadline:
313
+ break
314
+ sleep(0.1)
315
+ except Exception as error:
316
+ self.logger.error('Single-message reception failed: queue=%s error_type=%s', self.queue, type(error).__name__)
317
+ raise
318
+ self.logger.debug('Single-message wait ended: queue=%s', self.queue)
319
+ return
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABCMeta,
5
+ abstractmethod,
6
+ )
7
+
8
+ from json import (
9
+ dumps,
10
+ loads,
11
+ )
12
+ from typing import Any
13
+
14
+ __all__ = (
15
+ 'Encoder',
16
+ 'Decoder',
17
+ 'JavaScriptObjectNotationEncoder',
18
+ 'JavaScriptObjectNotationDecoder',
19
+ )
20
+
21
+ class Encoder(metaclass=ABCMeta):
22
+ """Encoder Interface"""
23
+ @abstractmethod
24
+ def __call__(self, obj: Any) -> bytes:
25
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
26
+
27
+ @property
28
+ @abstractmethod
29
+ def format(self) -> str:
30
+ raise NotImplementedError('{} must be implemented format'.format(self.__class__.__name__))
31
+
32
+ @property
33
+ @abstractmethod
34
+ def charset(self) -> str:
35
+ raise NotImplementedError('{} must be implemented charset'.format(self.__class__.__name__))
36
+
37
+
38
+ class Decoder(metaclass=ABCMeta):
39
+ """Decoder Interface"""
40
+ @abstractmethod
41
+ def __call__(self, obj: bytes, format: str, charset: str) -> Any:
42
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
43
+
44
+
45
+ class JavaScriptObjectNotationEncoder(Encoder):
46
+ """JavaScriptObjectNotation Encoder"""
47
+ def __call__(self, obj: Any) -> bytes:
48
+ return dumps(obj, ensure_ascii=False).encode(self.charset)
49
+
50
+ @property
51
+ def format(self) -> str: return 'application/json'
52
+
53
+ @property
54
+ def charset(self) -> str: return 'utf-8'
55
+
56
+
57
+ class JavaScriptObjectNotationDecoder(Decoder):
58
+ """JavaScriptObjectNotation Decoder"""
59
+ def __call__(self, obj: bytes, format: str, charset: str) -> Any:
60
+ if format != 'application/json': raise ValueError('format is not support')
61
+ return loads(obj.decode(charset))