DefStream-RabbitMQ 0.1.3__7-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,242 @@
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
+ logger = getLogger(__name__)
37
+
38
+
39
+ class Consumer(BaseConsumer):
40
+ """Consumer of Event Broker for RabbitMQ"""
41
+ def __init__(
42
+ self,
43
+ connection: BlockingConnection,
44
+ decode: Decoder,
45
+ handler: Optional[EventHandler] = None,
46
+ qos: int = 1,
47
+ offset: int = None,
48
+ encode: Optional[Encoder] = None,
49
+ ):
50
+ self.connection = connection
51
+ self.decode = decode
52
+ self.channel = self.connection.channel()
53
+ self.qos = qos
54
+ self.channel.basic_qos(
55
+ prefetch_count=qos,
56
+ global_qos=False,
57
+ )
58
+ self.channel.auto_decode = False
59
+ self.offset = offset
60
+ self.handler = handler
61
+ self.encode = encode if encode is not None else JavaScriptObjectNotationEncoder()
62
+ self.response_channel = None
63
+ self.queues = set()
64
+ self.stopped = False
65
+ return
66
+
67
+ def __del__(self):
68
+ self.close()
69
+ return
70
+
71
+ def subs(
72
+ self,
73
+ queue: Any,
74
+ handler: Optional[EventHandler] = None,
75
+ ) -> None:
76
+ if handler is None:
77
+ handler = self.handler
78
+ if handler is None:
79
+ raise ValueError('subs requires a handler or a default handler')
80
+ if isinstance(
81
+ queue,
82
+ str,
83
+ ):
84
+ subscription = Subscription(
85
+ queue=queue,
86
+ offset=self.offset,
87
+ )
88
+ else:
89
+ subscription = Subscription.parse(queue)
90
+ if subscription.queue in self.queues:
91
+ raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
92
+ self.channel.basic_qos(
93
+ prefetch_count=self.qos if subscription.qos is None else subscription.qos,
94
+ global_qos=False,
95
+ )
96
+ self.channel.basic_consume(
97
+ subscription.queue,
98
+ Callback(
99
+ self.decode,
100
+ handler,
101
+ subscription.queue,
102
+ ),
103
+ auto_ack=False,
104
+ exclusive=False,
105
+ arguments={'x-stream-offset': subscription.offset} if subscription.offset is not None else None,
106
+ )
107
+ self.queues.add(subscription.queue)
108
+
109
+ def call(self, callback: Callable[[], None]) -> None:
110
+ self.connection.add_callback_threadsafe(callback)
111
+
112
+ def fail(
113
+ self,
114
+ event: BaseEvent,
115
+ policy: Optional[EventError] = None,
116
+ ) -> None:
117
+ if isinstance(
118
+ event,
119
+ Event,
120
+ ) and event.properties.reply_to:
121
+ event = FailedRequest(
122
+ self,
123
+ event,
124
+ )
125
+ (policy if policy is not None else Requeue())(event)
126
+
127
+ def complete(
128
+ self,
129
+ event: BaseEvent,
130
+ result: Any,
131
+ ) -> None:
132
+ if isinstance(
133
+ event,
134
+ Event,
135
+ ) and event.properties.reply_to:
136
+ self.respond(
137
+ event,
138
+ {
139
+ 'ok': True,
140
+ 'result': result,
141
+ },
142
+ )
143
+ event.ack()
144
+
145
+ def respond(
146
+ self,
147
+ event: Event,
148
+ response: Any,
149
+ ) -> None:
150
+ if not event.properties.correlation_id:
151
+ raise ValueError('RPC request requires correlation_id')
152
+ channel = self.response_channel
153
+ if channel is None:
154
+ channel = self.connection.channel()
155
+ channel.confirm_delivery()
156
+ self.response_channel = channel
157
+ try:
158
+ channel.basic_publish(
159
+ exchange='',
160
+ routing_key=event.properties.reply_to,
161
+ body=self.encode(response),
162
+ properties=BasicProperties(
163
+ correlation_id=event.properties.correlation_id,
164
+ content_type=self.encode.format,
165
+ content_encoding=self.encode.charset,
166
+ ),
167
+ mandatory=True,
168
+ )
169
+ except UnroutableError:
170
+ logger.warning(
171
+ 'RPC reply is unroutable; settling request without a response: correlation_id=%s reply_to=%s',
172
+ event.properties.correlation_id,
173
+ event.properties.reply_to,
174
+ )
175
+
176
+ def close(self) -> None:
177
+ response_channel = getattr(self, 'response_channel', None)
178
+ if response_channel is not None and response_channel.is_open:
179
+ response_channel.close()
180
+ channel = getattr(self, 'channel', None)
181
+ if channel is not None and channel.is_open:
182
+ channel.close()
183
+
184
+ def run(self):
185
+ self.channel.start_consuming()
186
+ return
187
+
188
+ def stop(self):
189
+ if not self.stopped:
190
+ self.stopped = True
191
+ self.channel.stop_consuming()
192
+ return
193
+
194
+
195
+ class FailedRequest(BaseEvent):
196
+ def __init__(
197
+ self,
198
+ consumer: Consumer,
199
+ event: Event,
200
+ ):
201
+ self.consumer = consumer
202
+ self.event = event
203
+
204
+ @property
205
+ def body(self) -> Any:
206
+ return self.event.body
207
+
208
+ def ack(self) -> None:
209
+ self.event.ack()
210
+
211
+ def nack(self) -> None:
212
+ self.event.nack()
213
+
214
+ def reject(self) -> None:
215
+ self.consumer.respond(
216
+ self.event,
217
+ {
218
+ 'ok': False,
219
+ 'error': {
220
+ 'code': 'ProcessingError',
221
+ 'message': 'Request processing failed',
222
+ },
223
+ },
224
+ )
225
+ self.event.reject()
226
+
227
+
228
+ class Callback(object):
229
+ def __init__(self, decode: Decoder, handler: EventHandler, queue: str):
230
+ self.decode = decode
231
+ self.handler = handler
232
+ self.queue = queue
233
+ return
234
+ def __call__(self, channel, method, properties, body):
235
+ return self.handler(Event(
236
+ channel,
237
+ self.queue,
238
+ method.consumer_tag,
239
+ method.delivery_tag,
240
+ properties,
241
+ self.decode(body, properties.content_type, properties.content_encoding),
242
+ ))
@@ -0,0 +1,106 @@
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
+
10
+ __all__ = (
11
+ 'Event',
12
+ 'EventHandler',
13
+ )
14
+
15
+
16
+ class Event(BaseEvent):
17
+ """Event of Event Broker for RabbitMQ"""
18
+ def __init__(
19
+ self,
20
+ channel,
21
+ queue,
22
+ consumer,
23
+ transaction,
24
+ properties,
25
+ body,
26
+ ):
27
+ self.channel = channel
28
+ self.queue = queue
29
+ self.consumer = consumer
30
+ self.transaction = transaction
31
+ self.properties = properties
32
+ self.props = properties.headers
33
+ self.payload = body
34
+ return
35
+
36
+ def __repr__(self):
37
+ return 'Event(id={}, body={}, type={}, headers={})'.format(
38
+ self.properties.message_id,
39
+ self.payload,
40
+ self.properties.type,
41
+ self.props,
42
+ )
43
+
44
+ def ack(self):
45
+ self.channel.basic_ack(self.transaction)
46
+ return
47
+
48
+ def nack(self):
49
+ self.channel.basic_nack(
50
+ self.transaction,
51
+ requeue=True,
52
+ )
53
+ return
54
+
55
+ def reject(self):
56
+ self.channel.basic_reject(
57
+ self.transaction,
58
+ requeue=False,
59
+ )
60
+ return
61
+
62
+ def headers(self):
63
+ return self.props
64
+
65
+ def header(
66
+ self,
67
+ key,
68
+ ):
69
+ if key not in self.props:
70
+ return None
71
+ return self.props[key]
72
+
73
+ @property
74
+ def src(self):
75
+ return self.queue
76
+
77
+ @property
78
+ def id(self):
79
+ return self.properties.message_id
80
+
81
+ @property
82
+ def type(self):
83
+ return self.properties.type
84
+
85
+ @property
86
+ def body(self):
87
+ return self.payload
88
+
89
+ @property
90
+ def priority(self):
91
+ if not hasattr(
92
+ self.properties,
93
+ 'priority',
94
+ ):
95
+ return None
96
+ return self.properties.priority
97
+
98
+
99
+ class EventHandler(BaseEventHandler):
100
+ """Event Handler Interface of Event Broker for RabbitMQ"""
101
+ @abstractmethod
102
+ def __call__(
103
+ self,
104
+ event: Event,
105
+ ):
106
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
@@ -0,0 +1,227 @@
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 math import (
16
+ ceil,
17
+ isfinite,
18
+ )
19
+
20
+ from pika import (
21
+ BlockingConnection,
22
+ BasicProperties,
23
+ ConnectionParameters,
24
+ )
25
+
26
+ from time import (
27
+ time,
28
+ monotonic,
29
+ )
30
+ from enum import Enum
31
+ from uuid import uuid4
32
+
33
+ from typing import (
34
+ Any,
35
+ Dict,
36
+ Optional,
37
+ )
38
+
39
+ __all__ = (
40
+ 'Exchange',
41
+ )
42
+
43
+
44
+ class Exchange(
45
+ Sendable,
46
+ Callable,
47
+ ):
48
+ """Exchange of Event Broker for RabbitMQ"""
49
+ def __init__(
50
+ self,
51
+ connection: BlockingConnection,
52
+ encode: Encoder,
53
+ name: str = None,
54
+ decode: Optional[Decoder] = None,
55
+ rpc_parameters: Optional[ConnectionParameters] = None,
56
+ ):
57
+ self.connection = connection
58
+ self.encode = encode
59
+ self.channel = self.connection.channel()
60
+ self.channel.auto_decode = False
61
+ self.exchange = name
62
+ self.decode = decode
63
+ self.rpc_parameters = rpc_parameters
64
+ return
65
+
66
+ def __del__(self):
67
+ if self.channel and self.channel.is_open:
68
+ self.channel.close()
69
+ return
70
+
71
+ def __str__(self): return self.exchange
72
+
73
+ def send(
74
+ self,
75
+ body,
76
+ event: str = None,
77
+ headers: Dict = {},
78
+ priority: Optional[int] = None,
79
+ expiration: int = None,
80
+ timestamp: int = None,
81
+ persistent: bool = True,
82
+ id: str = None,
83
+ ):
84
+ validate_priority(
85
+ priority,
86
+ 255,
87
+ )
88
+ if not timestamp: timestamp = int(time())
89
+ if not id: id = uuid4().hex
90
+ properties = BasicProperties(
91
+ type=event if event else '',
92
+ headers=headers,
93
+ content_type=self.encode.format,
94
+ content_encoding=self.encode.charset,
95
+ priority=priority,
96
+ timestamp=timestamp,
97
+ expiration=expiration,
98
+ message_id=id,
99
+ delivery_mode=2 if persistent else 1,
100
+ )
101
+ self.channel.basic_publish(
102
+ exchange=self.exchange,
103
+ routing_key=event if event else '',
104
+ properties=properties,
105
+ body=self.encode(body),
106
+ )
107
+ return id
108
+
109
+ def call(
110
+ self,
111
+ body: Any,
112
+ timeout: float = 5,
113
+ *,
114
+ event: Optional[str] = None,
115
+ ) -> Any:
116
+ if self.rpc_parameters is None or self.decode is None:
117
+ raise RuntimeError('RPC requires decoder and connection parameters; obtain the exchange through Connection.exchange')
118
+ if isinstance(
119
+ timeout,
120
+ bool,
121
+ ) or not isinstance(
122
+ timeout,
123
+ (
124
+ int,
125
+ float,
126
+ ),
127
+ ) or not isfinite(timeout) or timeout <= 0:
128
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
129
+ deadline = monotonic() + timeout
130
+ settings = deepcopy(self.rpc_parameters)
131
+ settings.connection_attempts = 1
132
+ settings.socket_timeout = min(
133
+ settings.socket_timeout,
134
+ timeout,
135
+ )
136
+ settings.stack_timeout = timeout
137
+ settings.blocked_connection_timeout = timeout
138
+ connection = BlockingConnection(settings)
139
+ try:
140
+ channel = connection.channel()
141
+ reply_queue = channel.queue_declare(
142
+ queue='',
143
+ exclusive=True,
144
+ auto_delete=True,
145
+ ).method.queue
146
+ channel.confirm_delivery()
147
+ correlation_id = uuid4().hex
148
+ responses = []
149
+
150
+ def receive(
151
+ channel,
152
+ method,
153
+ properties,
154
+ payload,
155
+ ):
156
+ if properties.correlation_id == correlation_id:
157
+ responses.append((
158
+ properties,
159
+ payload,
160
+ ))
161
+
162
+ channel.basic_consume(
163
+ reply_queue,
164
+ on_message_callback=receive,
165
+ auto_ack=True,
166
+ )
167
+ remaining = deadline - monotonic()
168
+ if remaining <= 0:
169
+ raise TimeoutError('RPC timed out before request publication')
170
+ channel.basic_publish(
171
+ exchange=self.exchange,
172
+ routing_key=event if event is not None else '',
173
+ body=self.encode(body),
174
+ properties=BasicProperties(
175
+ reply_to=reply_queue,
176
+ correlation_id=correlation_id,
177
+ content_type=self.encode.format,
178
+ content_encoding=self.encode.charset,
179
+ expiration=str(max(
180
+ 1,
181
+ ceil(remaining * 1000),
182
+ )),
183
+ ),
184
+ mandatory=True,
185
+ )
186
+ while not responses:
187
+ remaining = deadline - monotonic()
188
+ if remaining <= 0:
189
+ raise TimeoutError(f'RPC response timed out for exchange {self.exchange!r} routing_key={event!r}')
190
+ connection.process_data_events(time_limit=min(
191
+ remaining,
192
+ 0.1,
193
+ ))
194
+ properties, payload = responses[0]
195
+ response = self.decode(
196
+ payload,
197
+ properties.content_type,
198
+ properties.content_encoding,
199
+ )
200
+ if not isinstance(
201
+ response,
202
+ dict,
203
+ ) or type(response.get('ok')) is not bool:
204
+ raise ValueError('Invalid RPC response envelope')
205
+ if response['ok']:
206
+ if 'result' not in response:
207
+ raise ValueError('RPC response has no result')
208
+ return response['result']
209
+ error = response.get('error')
210
+ if not isinstance(
211
+ error,
212
+ dict,
213
+ ) or not isinstance(
214
+ error.get('code'),
215
+ str,
216
+ ) or not isinstance(
217
+ error.get('message'),
218
+ str,
219
+ ):
220
+ raise ValueError('Invalid RPC error response')
221
+ raise RemoteError(
222
+ error['code'],
223
+ error['message'],
224
+ )
225
+ finally:
226
+ if connection.is_open:
227
+ connection.close()
@@ -0,0 +1,9 @@
1
+ from DefStream.Broker import (
2
+ Requeue,
3
+ Reject,
4
+ )
5
+
6
+ __all__ = (
7
+ 'Requeue',
8
+ 'Reject',
9
+ )