DefStream-RabbitMQ 0.1.24__110-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.
@@ -0,0 +1,49 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import Configuration as BaseConfiguration
4
+
5
+ from .Serializer import (
6
+ Encoder,
7
+ Decoder,
8
+ JavaScriptObjectNotationEncoder,
9
+ JavaScriptObjectNotationDecoder,
10
+ )
11
+
12
+
13
+ from pika import SSLOptions
14
+ from ssl import create_default_context
15
+ from typing import Optional
16
+
17
+ __all__ = (
18
+ 'Configuration',
19
+ )
20
+
21
+
22
+ class Configuration(BaseConfiguration):
23
+ """
24
+ Configuration of Event Broker for RabbitMQ
25
+ """
26
+ def __init__(
27
+ self,
28
+ host: str,
29
+ port: int,
30
+ username: str,
31
+ password: str,
32
+ ssl: bool = False,
33
+ vhost: str = '/',
34
+ timeout: Optional[int] = None,
35
+ heartbeat: Optional[int] = None,
36
+ encode: Encoder = JavaScriptObjectNotationEncoder(),
37
+ decode: Decoder = JavaScriptObjectNotationDecoder(),
38
+ ):
39
+ self.host = host
40
+ self.port = port
41
+ self.username = username
42
+ self.password = password
43
+ self.ssl = SSLOptions(create_default_context(), host) if ssl else None
44
+ self.vhost = vhost
45
+ self.timeout = timeout/1000 if timeout else None
46
+ self.heartbeat = int(heartbeat*2/1000) if heartbeat else None
47
+ self.encode = encode
48
+ self.decode = decode
49
+ return
@@ -0,0 +1,550 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Connection as BaseConnection,
5
+ GetExchange,
6
+ GetQueue,
7
+ GetStream,
8
+ GetConsumer,
9
+ Declarable,
10
+ PrioritySupport,
11
+ EventHandler,
12
+ )
13
+
14
+ from .Configuration import Configuration
15
+
16
+ from .Exchange import Exchange
17
+ from .Queue import Queue
18
+ from .Stream import Stream
19
+ from .Consumer import Consumer
20
+ from .Subscription import Subscription
21
+
22
+ from pika import (
23
+ BlockingConnection,
24
+ ConnectionParameters,
25
+ PlainCredentials,
26
+ BasicProperties,
27
+ )
28
+
29
+ from collections.abc import (
30
+ Mapping,
31
+ KeysView,
32
+ ItemsView,
33
+ ValuesView,
34
+ )
35
+ from operator import (
36
+ eq,
37
+ ne,
38
+ )
39
+ from enum import Enum
40
+ from uuid import uuid4
41
+ from contextlib import contextmanager
42
+ from logging import getLogger
43
+
44
+ from typing import (
45
+ Any as AnyValue,
46
+ Dict,
47
+ Iterator,
48
+ Union,
49
+ Optional,
50
+ )
51
+
52
+ __all__ = (
53
+ 'Connection',
54
+ 'ExchangeType',
55
+ 'Parameters',
56
+ 'All',
57
+ 'Any',
58
+ )
59
+
60
+
61
+ class ExchangeType(str, Enum):
62
+ Direct = 'direct'
63
+ FanOut = 'fanout'
64
+ Topic = 'topic'
65
+ Header = 'headers'
66
+ def __str__(self): return str(self.value)
67
+
68
+
69
+ class Parameters(Mapping):
70
+ """Bind Parameters Event Broker for RabbitMQ"""
71
+ def __init__(self, **kwargs):
72
+ self.args = kwargs
73
+ return
74
+
75
+ def __iter__(self) -> Iterator:
76
+ return self.args.__iter__()
77
+
78
+ def __reversed__(self) -> Iterator:
79
+ return self.args.__reversed__()
80
+
81
+ def __len__(self) -> int:
82
+ return self.args.__len__()
83
+
84
+ def __getitem__(
85
+ self,
86
+ key: AnyValue,
87
+ ) -> AnyValue:
88
+ return self.args.__getitem__(key)
89
+
90
+ def __contains__(self, key: object) -> bool:
91
+ return self.args.__contains__(key)
92
+
93
+ def __eq__(self, other) -> bool:
94
+ return eq(self.args, other)
95
+
96
+ def __ne__(self, other) -> bool:
97
+ return ne(self.args, other)
98
+
99
+ def keys(self) -> KeysView:
100
+ return self.args.keys()
101
+
102
+ def items(self) -> ItemsView:
103
+ return self.args.items()
104
+
105
+ def values(self) -> ValuesView:
106
+ return self.args.values()
107
+
108
+
109
+ class Connection(
110
+ BaseConnection,
111
+ GetExchange,
112
+ GetQueue,
113
+ GetStream,
114
+ GetConsumer,
115
+ Declarable,
116
+ PrioritySupport,
117
+ ):
118
+ """Connection of Event Broker for RabbitMQ"""
119
+ logger = getLogger('Broker.RabbitMQ.Connection')
120
+
121
+ def __init__(self, conf: Configuration):
122
+ self.conf = conf
123
+ self.parameters = ConnectionParameters(
124
+ host=self.conf.host,
125
+ port=self.conf.port,
126
+ credentials=PlainCredentials(self.conf.username, self.conf.password),
127
+ virtual_host=self.conf.vhost,
128
+ ssl_options=self.conf.ssl,
129
+ blocked_connection_timeout=self.conf.timeout,
130
+ heartbeat=self.conf.heartbeat
131
+ )
132
+ self.connection = None
133
+ self.publisher = None
134
+ return
135
+
136
+ def __del__(self):
137
+ self.close()
138
+ return
139
+
140
+ def connect(self):
141
+ if self.connection and self.connection.is_open:
142
+ self.logger.debug('Reusing open connection')
143
+ return
144
+ self.logger.debug('Connecting to RabbitMQ: host=%s port=%s vhost=%s', self.conf.host, self.conf.port, self.conf.vhost)
145
+ with self.observe('RabbitMQ connection'):
146
+ self.connection = BlockingConnection(parameters=self.parameters)
147
+ return
148
+
149
+ @contextmanager
150
+ def observe(
151
+ self,
152
+ operation: str,
153
+ *context,
154
+ ):
155
+ """Log failures only at I/O boundaries owned by this class, without exposing exception details."""
156
+ try:
157
+ yield
158
+ except Exception as error:
159
+ self.logger.error(operation + ' failed: error_type=%s', *context, type(error).__name__)
160
+ raise
161
+ else:
162
+ self.logger.debug(operation + ' completed', *context)
163
+ return
164
+
165
+ def connected(self):
166
+ if self.connection is None or not self.connection.is_open:
167
+ self.logger.error('RabbitMQ connection is not open')
168
+ raise RuntimeError('Connect the RabbitMQ connection before use')
169
+ return self.connection
170
+
171
+ def declare(
172
+ self,
173
+ parameters: AnyValue,
174
+ ) -> None:
175
+ subscription: Subscription = Subscription.parse(parameters)
176
+ self.logger.debug('Task queue declaration: queue=%s error_queue=%s max_priority=%s durable=True', subscription.queue, subscription.error, subscription.max_priority)
177
+ connection = self.connected()
178
+ with self.observe('Task queue declaration'):
179
+ channel = connection.channel()
180
+ try:
181
+ arguments: Dict[str, AnyValue] = {}
182
+ if subscription.error is not None:
183
+ channel.queue_declare(
184
+ subscription.error,
185
+ durable=True,
186
+ )
187
+ arguments = {
188
+ 'x-dead-letter-exchange': '',
189
+ 'x-dead-letter-routing-key': subscription.error,
190
+ }
191
+ if subscription.max_priority is not None:
192
+ arguments['x-max-priority'] = subscription.max_priority
193
+ if subscription.timeout is not None:
194
+ arguments['x-consumer-timeout'] = subscription.timeout
195
+ channel.queue_declare(
196
+ subscription.queue,
197
+ durable=True,
198
+ arguments=arguments,
199
+ )
200
+ finally:
201
+ if channel.is_open:
202
+ channel.close()
203
+ return
204
+
205
+ def priority_parameters(
206
+ self,
207
+ parameters: AnyValue,
208
+ ) -> Subscription:
209
+ """작업 기본값을 검증하고 선언·발행에 사용할 구독 설정을 반환한다."""
210
+ return Subscription.parse(parameters)
211
+
212
+ def emit(
213
+ self,
214
+ body: AnyValue,
215
+ parameters: AnyValue,
216
+ *,
217
+ priority: Optional[int] = None,
218
+ ) -> str:
219
+ subscription = Subscription.parse(parameters)
220
+ if priority is None:
221
+ priority = subscription.priority
222
+ subscription.validate_priority(priority)
223
+ connection = self.connected()
224
+ channel = self.publisher
225
+ if channel is None or not channel.is_open:
226
+ try:
227
+ channel = connection.channel()
228
+ channel.confirm_delivery()
229
+ self.logger.debug('Publisher channel created: publisher_confirm=True')
230
+ except Exception as error:
231
+ self.logger.error('Publisher channel creation failed: queue=%s error_type=%s', subscription.queue, type(error).__name__)
232
+ if channel is not None and channel.is_open:
233
+ channel.close()
234
+ raise
235
+ self.publisher = channel
236
+ else:
237
+ self.logger.debug('Reusing publisher channel: publisher_confirm=True')
238
+ id = uuid4().hex
239
+ self.logger.debug('Publishing directly: queue=%s message_id=%s priority=%s publisher_confirm=True', subscription.queue, id, priority)
240
+ try:
241
+ channel.basic_publish(
242
+ exchange='',
243
+ routing_key=subscription.queue,
244
+ body=self.conf.encode(body),
245
+ properties=BasicProperties(
246
+ message_id=id,
247
+ headers={},
248
+ content_type=self.conf.encode.format,
249
+ content_encoding=self.conf.encode.charset,
250
+ delivery_mode=2,
251
+ priority=priority,
252
+ ),
253
+ mandatory=True,
254
+ )
255
+ except Exception as error:
256
+ self.logger.error('Direct publication failed: queue=%s message_id=%s error_type=%s', subscription.queue, id, type(error).__name__)
257
+ raise
258
+ self.logger.debug('Direct publication confirmed: queue=%s message_id=%s', subscription.queue, id)
259
+ return id
260
+
261
+ def request(
262
+ self,
263
+ body: AnyValue,
264
+ parameters: AnyValue,
265
+ timeout: float = 5,
266
+ *,
267
+ priority: Optional[int] = None,
268
+ ) -> AnyValue:
269
+ subscription = Subscription.parse(parameters)
270
+ if priority is None:
271
+ priority = subscription.priority
272
+ subscription.validate_priority(priority)
273
+ self.connected()
274
+ queue = self.queue(subscription.queue)
275
+ self.logger.debug('Delegating RPC to Queue: queue=%s timeout=%s priority=%s', subscription.queue, timeout, priority)
276
+ try:
277
+ if priority is not None:
278
+ return queue.call(
279
+ body,
280
+ timeout=timeout,
281
+ priority=priority,
282
+ )
283
+ return queue.call(
284
+ body,
285
+ timeout=timeout,
286
+ )
287
+ finally:
288
+ try:
289
+ if queue.channel.is_open:
290
+ queue.channel.close()
291
+ self.logger.debug('RPC delegated channel cleanup completed: queue=%s', subscription.queue)
292
+ except Exception as error:
293
+ self.logger.error('RPC delegated channel cleanup failed: queue=%s error_type=%s', subscription.queue, type(error).__name__)
294
+ raise
295
+
296
+ def exchange(self, exchange: str) -> Exchange:
297
+ self.logger.debug('Creating Exchange: exchange=%s', exchange)
298
+ return Exchange(
299
+ self.connected(),
300
+ self.conf.encode,
301
+ exchange,
302
+ decode=self.conf.decode,
303
+ rpc_parameters=self.parameters,
304
+ )
305
+
306
+ def queue(self, queue: str) -> Queue:
307
+ self.logger.debug('Creating Queue: queue=%s', queue)
308
+ return Queue(
309
+ self.connected(),
310
+ self.conf.encode,
311
+ self.conf.decode,
312
+ queue,
313
+ rpc_parameters=self.parameters,
314
+ )
315
+
316
+ def stream(self, queue: str) -> Stream:
317
+ self.logger.debug('Creating Stream: queue=%s', queue)
318
+ return Stream(self.connected(), self.conf.encode, self.conf.decode, queue)
319
+
320
+ def consumer(
321
+ self,
322
+ handler: Optional[EventHandler] = None,
323
+ qos: int = 1,
324
+ offset: Optional[int] = None,
325
+ ) -> Consumer:
326
+ self.logger.debug('Creating Consumer: qos=%s offset=%s', qos, offset)
327
+ return Consumer(
328
+ self.connected(),
329
+ self.conf.decode,
330
+ handler,
331
+ qos=qos,
332
+ offset=offset,
333
+ encode=self.conf.encode,
334
+ )
335
+
336
+ def close(self):
337
+ connection = self.connection
338
+ self.connection = None
339
+ self.publisher = None
340
+ if connection and connection.is_open:
341
+ with self.observe('RabbitMQ connection close'):
342
+ connection.close()
343
+ return
344
+
345
+ def create_exchange(
346
+ self,
347
+ name: str,
348
+ type: ExchangeType,
349
+ durable: bool = True,
350
+ autodelete: bool = False,
351
+ alter: Optional[Union[str, Exchange]] = None,
352
+ ):
353
+ args: Dict[str, AnyValue] = {}
354
+ if alter:
355
+ args['alternate-exchange'] = str(alter)
356
+ self.logger.debug('Exchange declaration: exchange=%s type=%s durable=%s auto_delete=%s alternate_exchange=%s', name, type, durable, autodelete, str(alter) if alter else None)
357
+ with self.observe('Exchange declaration: exchange=%s', name):
358
+ channel = self.connected().channel()
359
+ try:
360
+ channel.exchange_declare(
361
+ name,
362
+ str(type),
363
+ durable=durable,
364
+ auto_delete=autodelete,
365
+ arguments=args
366
+ )
367
+ finally:
368
+ if channel.is_open:
369
+ channel.close()
370
+ return
371
+
372
+ def delete_exchange(
373
+ self,
374
+ name: str,
375
+ ):
376
+ with self.observe('Exchange deletion: exchange=%s', name):
377
+ channel = self.connected().channel()
378
+ channel.exchange_delete(name)
379
+ return
380
+
381
+ def bind_exchange_to_exchange(
382
+ self,
383
+ src: Union[str, Exchange],
384
+ to: Union[str, Exchange],
385
+ event: Optional[str] = None,
386
+ parameters: Optional[Parameters] = None,
387
+ ):
388
+ with self.observe('Exchange binding: src=%s to=%s routing_key=%s', str(src), str(to), event):
389
+ channel = self.connected().channel()
390
+ channel.exchange_bind(
391
+ str(src),
392
+ str(to),
393
+ routing_key=event if event else '',
394
+ arguments=dict(parameters) if parameters is not None else None
395
+ )
396
+ return
397
+
398
+ def unbind_exchange_to_exchange(
399
+ self,
400
+ src: Union[str, Exchange],
401
+ to: Union[str, Exchange],
402
+ event: Optional[str] = None,
403
+ parameters: Optional[Parameters] = None,
404
+ ):
405
+ with self.observe('Exchange unbinding: src=%s to=%s routing_key=%s', str(src), str(to), event):
406
+ channel = self.connected().channel()
407
+ channel.exchange_unbind(
408
+ str(src),
409
+ str(to),
410
+ routing_key=event if event else '',
411
+ arguments=dict(parameters) if parameters is not None else None,
412
+ )
413
+ return
414
+
415
+ def create_queue(
416
+ self,
417
+ name: str,
418
+ expires: Optional[int] = None,
419
+ ttl: Optional[int] = None,
420
+ limit: Optional[int] = None,
421
+ size: Optional[int] = None,
422
+ maxPriority: Optional[int] = None,
423
+ durable: bool = True,
424
+ autodelete: bool = False,
425
+ errorQueue: Optional[Union[str, Queue]] = None,
426
+ errorExchange: Optional[Union[str, Exchange]] = None,
427
+ ):
428
+ args: Dict[str, AnyValue] = {}
429
+ if errorQueue:
430
+ args['x-dead-letter-exchange'] = ''
431
+ args['x-dead-letter-routing-key'] = str(errorQueue)
432
+ if errorExchange:
433
+ args['x-dead-letter-exchange'] = str(errorExchange)
434
+ if expires:
435
+ args['x-expires'] = expires
436
+ if ttl:
437
+ args['x-message-ttl'] = ttl
438
+ if limit:
439
+ args['x-max-length'] = limit
440
+ if size:
441
+ args['x-max-length-bytes'] = size
442
+ if maxPriority:
443
+ args['x-max-priority'] = maxPriority
444
+ self.logger.debug('Queue declaration options: queue=%s durable=%s auto_delete=%s expires=%s ttl=%s limit=%s size=%s max_priority=%s error_queue=%s error_exchange=%s', name, durable, autodelete, expires, ttl, limit, size, maxPriority, str(errorQueue) if errorQueue else None, str(errorExchange) if errorExchange else None)
445
+ with self.observe('Queue declaration: queue=%s', name):
446
+ channel = self.connected().channel()
447
+ channel.queue_declare(
448
+ name,
449
+ durable=durable,
450
+ auto_delete=autodelete,
451
+ arguments=args
452
+ )
453
+ return
454
+
455
+ def delete_queue(
456
+ self,
457
+ queue: str,
458
+ ):
459
+ with self.observe('Queue deletion: queue=%s', queue):
460
+ channel = self.connected().channel()
461
+ channel.queue_delete(queue)
462
+ return
463
+
464
+ def bind_exchange_to_queue(
465
+ self,
466
+ src: Union[str, Exchange],
467
+ to: Union[str, Queue],
468
+ event: Optional[str] = None,
469
+ parameters: Optional[Parameters] = None,
470
+ ):
471
+ with self.observe('Queue binding: exchange=%s queue=%s routing_key=%s', str(src), str(to), event):
472
+ channel = self.connected().channel()
473
+ channel.queue_bind(
474
+ str(to),
475
+ str(src),
476
+ routing_key=event if event else '',
477
+ arguments=dict(parameters) if parameters is not None else None
478
+ )
479
+ return
480
+
481
+ def unbind_exchange_to_queue(
482
+ self,
483
+ src: Union[str, Exchange],
484
+ to: Union[str, Queue],
485
+ event: Optional[str] = None,
486
+ parameters: Optional[Parameters] = None,
487
+ ):
488
+ with self.observe('Queue unbinding: exchange=%s queue=%s routing_key=%s', str(src), str(to), event):
489
+ channel = self.connected().channel()
490
+ channel.queue_unbind(
491
+ str(to),
492
+ str(src),
493
+ routing_key=event if event else '',
494
+ arguments=dict(parameters) if parameters is not None else None,
495
+ )
496
+ return
497
+
498
+ def create_stream(
499
+ self,
500
+ name: str,
501
+ size: Optional[int] = None,
502
+ durable: bool = True,
503
+ autodelete: bool = False,
504
+ ):
505
+ args: Dict[str, AnyValue] = {
506
+ 'x-queue-type': 'stream',
507
+ }
508
+ if size:
509
+ args['x-max-length-bytes'] = size
510
+ self.logger.debug('Stream declaration options: queue=%s durable=%s auto_delete=%s size=%s', name, durable, autodelete, size)
511
+ with self.observe('Stream declaration: queue=%s', name):
512
+ channel = self.connected().channel()
513
+ channel.queue_declare(
514
+ name,
515
+ durable=durable,
516
+ auto_delete=autodelete,
517
+ arguments=args
518
+ )
519
+ return
520
+
521
+ def delete_stream(
522
+ self,
523
+ queue: str,
524
+ ):
525
+ with self.observe('Stream deletion: queue=%s', queue):
526
+ channel = self.connected().channel()
527
+ channel.queue_delete(queue)
528
+ return
529
+
530
+
531
+ class All(Parameters):
532
+ """All Bind Parameters Event Broker for RabbitMQ"""
533
+ def __init__(self, **kwargs):
534
+ headers = {
535
+ 'x-match': 'all-with-x'
536
+ }
537
+ headers.update(kwargs)
538
+ super().__init__(**headers)
539
+ return
540
+
541
+
542
+ class Any(Parameters):
543
+ """Any Bind Parameters Event Broker for RabbitMQ"""
544
+ def __init__(self, **kwargs):
545
+ headers = {
546
+ 'x-match': 'any-with-x'
547
+ }
548
+ headers.update(kwargs)
549
+ super().__init__(**headers)
550
+ return