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,359 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Consumer as BaseConsumer,
5
+ EventHandler,
6
+ Event as BaseEvent,
7
+ EventError,
8
+ )
9
+
10
+ from .Serializer import (
11
+ Decoder,
12
+ Encoder,
13
+ JavaScriptObjectNotationEncoder,
14
+ )
15
+ from logging import getLogger
16
+
17
+ from .Event import Event
18
+ from .Policies import Requeue
19
+ from .Subscription import Subscription
20
+ from typing import (
21
+ Any,
22
+ Callable,
23
+ Optional,
24
+ )
25
+
26
+ from pika import (
27
+ BasicProperties,
28
+ BlockingConnection,
29
+ )
30
+ from pika.exceptions import UnroutableError
31
+
32
+ __all__ = (
33
+ 'Consumer'
34
+ )
35
+
36
+ class Consumer(BaseConsumer):
37
+ """Consumer of Event Broker for RabbitMQ"""
38
+ logger = getLogger('DefStream.Broker.Implements.RabbitMQ.Consumer')
39
+
40
+ def __init__(
41
+ self,
42
+ connection: BlockingConnection,
43
+ decode: Decoder,
44
+ handler: Optional[EventHandler] = None,
45
+ qos: int = 1,
46
+ offset: int = None,
47
+ encode: Optional[Encoder] = None,
48
+ ):
49
+ self.connection = connection
50
+ self.decode = decode
51
+ self.qos = qos
52
+ try:
53
+ self.channel = self.connection.channel()
54
+ self.channel.basic_qos(
55
+ prefetch_count=qos,
56
+ global_qos=False,
57
+ )
58
+ except Exception as error:
59
+ self.logger.error('Consumption channel initialization failed: qos=%s error_type=%s', qos, type(error).__name__)
60
+ raise
61
+ self.logger.debug('Consumption channel created: qos=%s offset=%s', qos, offset)
62
+ self.channel.auto_decode = False
63
+ self.offset = offset
64
+ self.handler = handler
65
+ self.encode = encode if encode is not None else JavaScriptObjectNotationEncoder()
66
+ self.response_channel = None
67
+ self.queues = set()
68
+ self.stopped = False
69
+ self.callback_error = None
70
+ return
71
+
72
+ def __del__(self):
73
+ self.close()
74
+ return
75
+
76
+ def subs(
77
+ self,
78
+ queue: Any,
79
+ handler: Optional[EventHandler] = None,
80
+ ) -> None:
81
+ if handler is None:
82
+ handler = self.handler
83
+ if handler is None:
84
+ raise ValueError('subs requires a handler or a default handler')
85
+ if isinstance(
86
+ queue,
87
+ str,
88
+ ):
89
+ subscription = Subscription(
90
+ queue=queue,
91
+ offset=self.offset,
92
+ )
93
+ else:
94
+ subscription = Subscription.parse(queue)
95
+ if subscription.queue in self.queues:
96
+ raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
97
+ qos = self.qos if subscription.qos is None else subscription.qos
98
+ self.logger.debug('Applying subscription: queue=%s qos=%s offset=%s', subscription.queue, qos, subscription.offset)
99
+ try:
100
+ self.channel.basic_qos(
101
+ prefetch_count=qos,
102
+ global_qos=False,
103
+ )
104
+ tag = self.channel.basic_consume(
105
+ subscription.queue,
106
+ Callback(
107
+ self.decode,
108
+ handler,
109
+ subscription.queue,
110
+ logger=self.logger,
111
+ owner=self,
112
+ ),
113
+ auto_ack=False,
114
+ exclusive=False,
115
+ arguments={'x-stream-offset': subscription.offset} if subscription.offset is not None else None,
116
+ )
117
+ except Exception as error:
118
+ self.logger.error('Subscription failed: queue=%s error_type=%s', subscription.queue, type(error).__name__)
119
+ raise
120
+ self.queues.add(subscription.queue)
121
+ self.logger.info('Subscription applied: queue=%s consumer_tag=%s', subscription.queue, tag)
122
+ return
123
+
124
+ def call(self, callback: Callable[[], None]) -> None:
125
+ self.logger.debug('Scheduling callback on connection owner thread')
126
+ try:
127
+ self.connection.add_callback_threadsafe(callback)
128
+ except Exception as error:
129
+ self.logger.error('Callback scheduling failed: error_type=%s', type(error).__name__)
130
+ raise
131
+ self.logger.debug('Callback scheduled on connection owner thread')
132
+ return
133
+
134
+ def fail(
135
+ self,
136
+ event: BaseEvent,
137
+ policy: Optional[EventError] = None,
138
+ ) -> None:
139
+ self.logger.debug('Task failure policy selected: policy=%s', type(policy).__name__ if policy is not None else 'Requeue')
140
+ if isinstance(
141
+ event,
142
+ Event,
143
+ ) and event.properties.reply_to:
144
+ event = FailedRequest(
145
+ self,
146
+ event,
147
+ )
148
+ (policy if policy is not None else Requeue())(event)
149
+ return
150
+
151
+ def complete(
152
+ self,
153
+ event: BaseEvent,
154
+ result: Any,
155
+ ) -> None:
156
+ self.logger.debug('Task completion policy selected: ack')
157
+ if isinstance(
158
+ event,
159
+ Event,
160
+ ) and event.properties.reply_to:
161
+ self.logger.debug('Building RPC success response: queue=%s correlation_id=%s', event.queue, event.properties.correlation_id)
162
+ self.respond(
163
+ event,
164
+ {
165
+ 'ok': True,
166
+ 'result': result,
167
+ },
168
+ )
169
+ event.ack()
170
+ return
171
+
172
+ def respond(
173
+ self,
174
+ event: Event,
175
+ response: Any,
176
+ ) -> None:
177
+ if not event.properties.correlation_id:
178
+ self.logger.error('RPC response validation failed: missing correlation_id')
179
+ raise ValueError('RPC request requires correlation_id')
180
+ channel = self.response_channel
181
+ self.logger.debug('Attempting RPC response: correlation_id=%s reply_to=%s', event.properties.correlation_id, event.properties.reply_to)
182
+ if channel is None:
183
+ try:
184
+ channel = self.connection.channel()
185
+ channel.confirm_delivery()
186
+ self.response_channel = channel
187
+ self.logger.debug('RPC response channel created: publisher_confirm=True')
188
+ except Exception as error:
189
+ self.logger.error('RPC response channel creation failed: correlation_id=%s error_type=%s', event.properties.correlation_id, type(error).__name__)
190
+ raise
191
+ else:
192
+ self.logger.debug('Reusing RPC response channel')
193
+ try:
194
+ channel.basic_publish(
195
+ exchange='',
196
+ routing_key=event.properties.reply_to,
197
+ body=self.encode(response),
198
+ properties=BasicProperties(
199
+ correlation_id=event.properties.correlation_id,
200
+ content_type=self.encode.format,
201
+ content_encoding=self.encode.charset,
202
+ ),
203
+ mandatory=True,
204
+ )
205
+ except UnroutableError:
206
+ self.logger.warning(
207
+ 'RPC reply is unroutable; settling request without a response: correlation_id=%s reply_to=%s',
208
+ event.properties.correlation_id,
209
+ event.properties.reply_to,
210
+ )
211
+ except Exception as error:
212
+ self.logger.error('RPC response publication failed: correlation_id=%s error_type=%s', event.properties.correlation_id, type(error).__name__)
213
+ raise
214
+ else:
215
+ self.logger.debug('RPC response published: correlation_id=%s', event.properties.correlation_id)
216
+ return
217
+
218
+ def close(self) -> None:
219
+ response_channel = getattr(self, 'response_channel', None)
220
+ try:
221
+ if response_channel is not None and response_channel.is_open:
222
+ response_channel.close()
223
+ self.logger.info('RPC response channel closed')
224
+ channel = getattr(self, 'channel', None)
225
+ if channel is not None and channel.is_open:
226
+ channel.close()
227
+ self.logger.info('Consumption channel closed')
228
+ except Exception as error:
229
+ self.logger.error('Channel close failed: error_type=%s', type(error).__name__)
230
+ raise
231
+ return
232
+
233
+ def run(self):
234
+ self.logger.info('Starting consumption loop')
235
+ try:
236
+ self.channel.start_consuming()
237
+ except Exception as error:
238
+ # 정산 오류는 예약 콜백에서 루프로 전파되어도 Event의 기록을 재출력하지 않는다.
239
+ owned_boundaries = (
240
+ Event.ack.__code__,
241
+ Event.nack.__code__,
242
+ Event.reject.__code__,
243
+ Consumer.respond.__code__,
244
+ Consumer.stop.__code__,
245
+ )
246
+ trace = error.__traceback__
247
+ observed = error is self.callback_error
248
+ while trace is not None:
249
+ if trace.tb_frame.f_code in owned_boundaries:
250
+ observed = True
251
+ break
252
+ trace = trace.tb_next
253
+ if not observed:
254
+ self.logger.error('Consumption loop failed: error_type=%s', type(error).__name__)
255
+ raise
256
+ finally:
257
+ self.callback_error = None
258
+ self.logger.info('Consumption loop ended')
259
+ return
260
+
261
+ def stop(self):
262
+ if not self.stopped:
263
+ self.stopped = True
264
+ self.logger.info('Consumption loop stop requested')
265
+ try:
266
+ self.channel.stop_consuming()
267
+ except Exception as error:
268
+ self.logger.error('Failed to stop consumption loop: error_type=%s', type(error).__name__)
269
+ raise
270
+ self.logger.info('Consumption loop stopped')
271
+ return
272
+
273
+
274
+ class FailedRequest(BaseEvent):
275
+ def __init__(
276
+ self,
277
+ consumer: Consumer,
278
+ event: Event,
279
+ ):
280
+ self.consumer = consumer
281
+ self.event = event
282
+
283
+ @property
284
+ def body(self) -> Any:
285
+ return self.event.body
286
+
287
+ def ack(self) -> None:
288
+ self.event.ack()
289
+
290
+ def nack(self) -> None:
291
+ self.event.nack()
292
+
293
+ def reject(self) -> None:
294
+ self.consumer.logger.debug('Building RPC failure response: queue=%s correlation_id=%s', self.event.queue, self.event.properties.correlation_id)
295
+ self.consumer.respond(
296
+ self.event,
297
+ {
298
+ 'ok': False,
299
+ 'error': {
300
+ 'code': 'ProcessingError',
301
+ 'message': 'Request processing failed',
302
+ },
303
+ },
304
+ )
305
+ self.event.reject()
306
+ return
307
+
308
+
309
+ class Callback(object):
310
+ def __init__(
311
+ self,
312
+ decode: Decoder,
313
+ handler: EventHandler,
314
+ queue: str,
315
+ logger=None,
316
+ owner=None,
317
+ ):
318
+ self.decode = decode
319
+ self.handler = handler
320
+ self.queue = queue
321
+ self.logger = logger if logger is not None else Consumer.logger
322
+ self.owner = owner
323
+ return
324
+ def __call__(
325
+ self,
326
+ channel,
327
+ method,
328
+ properties,
329
+ body,
330
+ ):
331
+ try:
332
+ return self.deliver(channel, method, properties, body)
333
+ except Exception as error:
334
+ if self.owner is not None:
335
+ self.owner.callback_error = error
336
+ raise
337
+
338
+ def deliver(
339
+ self,
340
+ channel,
341
+ method,
342
+ properties,
343
+ body,
344
+ ):
345
+ self.logger.debug('Message received: queue=%s consumer_tag=%s delivery_tag=%s message_id=%s', self.queue, method.consumer_tag, method.delivery_tag, properties.message_id)
346
+ try:
347
+ decoded = self.decode(body, properties.content_type, properties.content_encoding)
348
+ except Exception as error:
349
+ self.logger.error('Message decoding failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
350
+ raise
351
+ self.logger.debug('Message decoded; dispatching to handler: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
352
+ return self.handler(Event(
353
+ channel,
354
+ self.queue,
355
+ method.consumer_tag,
356
+ method.delivery_tag,
357
+ properties,
358
+ decoded,
359
+ ))
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Event as BaseEvent,
5
+ EventHandler as BaseEventHandler,
6
+ )
7
+
8
+ from abc import abstractmethod
9
+ from logging import getLogger
10
+
11
+ __all__ = (
12
+ 'Event',
13
+ 'EventHandler',
14
+ )
15
+
16
+
17
+ class Event(BaseEvent):
18
+ """Event of Event Broker for RabbitMQ"""
19
+ logger = getLogger('DefStream.Broker.Implements.RabbitMQ.Event')
20
+
21
+ def __init__(
22
+ self,
23
+ channel,
24
+ queue,
25
+ consumer,
26
+ transaction,
27
+ properties,
28
+ body,
29
+ ):
30
+ self.channel = channel
31
+ self.queue = queue
32
+ self.consumer = consumer
33
+ self.transaction = transaction
34
+ self.properties = properties
35
+ self.props = properties.headers
36
+ self.payload = body
37
+ return
38
+
39
+ def __repr__(self):
40
+ return 'Event(id={}, body={}, type={}, headers={})'.format(
41
+ self.properties.message_id,
42
+ self.payload,
43
+ self.properties.type,
44
+ self.props,
45
+ )
46
+
47
+ def ack(self):
48
+ self.logger.debug('ack requested: queue=%s delivery_tag=%s message_id=%s', self.queue, self.transaction, self.id)
49
+ try:
50
+ self.channel.basic_ack(self.transaction)
51
+ except Exception as error:
52
+ self.logger.error('ack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
53
+ raise
54
+ self.logger.debug('ack broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
55
+ return
56
+
57
+ def nack(self):
58
+ self.logger.debug('nack requested: queue=%s delivery_tag=%s message_id=%s requeue=True', self.queue, self.transaction, self.id)
59
+ try:
60
+ self.channel.basic_nack(
61
+ self.transaction,
62
+ requeue=True,
63
+ )
64
+ except Exception as error:
65
+ self.logger.error('nack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
66
+ raise
67
+ self.logger.debug('nack broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
68
+ return
69
+
70
+ def reject(self):
71
+ self.logger.debug('reject requested: queue=%s delivery_tag=%s message_id=%s requeue=False', self.queue, self.transaction, self.id)
72
+ try:
73
+ self.channel.basic_reject(
74
+ self.transaction,
75
+ requeue=False,
76
+ )
77
+ except Exception as error:
78
+ self.logger.error('reject failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
79
+ raise
80
+ self.logger.debug('reject broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
81
+ return
82
+
83
+ def headers(self):
84
+ return self.props
85
+
86
+ def header(
87
+ self,
88
+ key,
89
+ ):
90
+ if key not in self.props:
91
+ return None
92
+ return self.props[key]
93
+
94
+ @property
95
+ def src(self):
96
+ return self.queue
97
+
98
+ @property
99
+ def id(self):
100
+ return self.properties.message_id
101
+
102
+ @property
103
+ def type(self):
104
+ return self.properties.type
105
+
106
+ @property
107
+ def body(self):
108
+ return self.payload
109
+
110
+ @property
111
+ def priority(self):
112
+ if not hasattr(
113
+ self.properties,
114
+ 'priority',
115
+ ):
116
+ return None
117
+ return self.properties.priority
118
+
119
+
120
+ class EventHandler(BaseEventHandler):
121
+ """Event Handler Interface of Event Broker for RabbitMQ"""
122
+ @abstractmethod
123
+ def __call__(
124
+ self,
125
+ event: Event,
126
+ ):
127
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))