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