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.
- DefStream/Broker/Implements/RabbitMQ/Configuration.py +48 -0
- DefStream/Broker/Implements/RabbitMQ/Connection.py +538 -0
- DefStream/Broker/Implements/RabbitMQ/Consumer.py +359 -0
- DefStream/Broker/Implements/RabbitMQ/Event.py +127 -0
- DefStream/Broker/Implements/RabbitMQ/Exchange.py +278 -0
- DefStream/Broker/Implements/RabbitMQ/Policies.py +9 -0
- DefStream/Broker/Implements/RabbitMQ/Queue.py +319 -0
- DefStream/Broker/Implements/RabbitMQ/Serializer.py +61 -0
- DefStream/Broker/Implements/RabbitMQ/Stream.py +179 -0
- DefStream/Broker/Implements/RabbitMQ/Subscription.py +74 -0
- DefStream/Broker/Implements/RabbitMQ/__init__.py +47 -0
- defstream_rabbitmq-0.1.5.dist-info/METADATA +306 -0
- defstream_rabbitmq-0.1.5.dist-info/RECORD +16 -0
- defstream_rabbitmq-0.1.5.dist-info/WHEEL +6 -0
- defstream_rabbitmq-0.1.5.dist-info/licenses/LICENSE +674 -0
- defstream_rabbitmq-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -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__))
|