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,260 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Callable,
6
+ Poppable,
7
+ RemoteError,
8
+ validate_priority,
9
+ )
10
+
11
+ from .Serializer import (
12
+ Encoder,
13
+ Decoder,
14
+ )
15
+ from .Event import Event
16
+ from copy import deepcopy
17
+ from math import (
18
+ ceil,
19
+ isfinite,
20
+ )
21
+
22
+ from pika import (
23
+ BlockingConnection,
24
+ BasicProperties,
25
+ ConnectionParameters,
26
+ )
27
+
28
+ from time import (
29
+ time,
30
+ sleep,
31
+ monotonic,
32
+ )
33
+ from uuid import uuid4
34
+
35
+ from typing import (
36
+ Any,
37
+ Optional,
38
+ Dict,
39
+ )
40
+
41
+ __all__ = (
42
+ 'Queue'
43
+ )
44
+
45
+
46
+ class Queue(
47
+ Sendable,
48
+ Callable,
49
+ Poppable,
50
+ ):
51
+ """Queue of Event Broker for RabbitMQ"""
52
+ def __init__(
53
+ self,
54
+ connection: BlockingConnection,
55
+ encode: Encoder,
56
+ decode: Decoder,
57
+ name: str = None,
58
+ rpc_parameters: Optional[ConnectionParameters] = None,
59
+ ):
60
+ self.connection = connection
61
+ self.encode = encode
62
+ self.decode = decode
63
+ self.channel = self.connection.channel()
64
+ self.channel.auto_decode = False
65
+ self.queue = name
66
+ self.event = None
67
+ self.rpc_parameters = rpc_parameters
68
+ return
69
+
70
+ def __del__(self):
71
+ if self.channel and self.channel.is_open:
72
+ self.channel.close()
73
+ return
74
+
75
+ def __str__(self): return self.queue
76
+
77
+ def send(
78
+ self,
79
+ body,
80
+ event: str = None,
81
+ headers: Dict = {},
82
+ priority: Optional[int] = None,
83
+ expiration: int = None,
84
+ timestamp: int = None,
85
+ persistent: bool = True,
86
+ id: str = None,
87
+ reply_to: str = None,
88
+ correlation_id: str = None,
89
+ ):
90
+ validate_priority(
91
+ priority,
92
+ 255,
93
+ )
94
+ if not timestamp: timestamp = int(time())
95
+ if not id: id = uuid4().hex
96
+ properties = BasicProperties(
97
+ type=event if event else '',
98
+ headers=headers,
99
+ content_type=self.encode.format,
100
+ content_encoding=self.encode.charset,
101
+ priority=priority,
102
+ timestamp=timestamp,
103
+ expiration=expiration,
104
+ message_id=id,
105
+ delivery_mode=2 if persistent else 1,
106
+ reply_to=reply_to,
107
+ correlation_id=correlation_id,
108
+ )
109
+ self.channel.basic_publish(
110
+ exchange='',
111
+ routing_key=self.queue,
112
+ properties=properties,
113
+ body=self.encode(body)
114
+ )
115
+ return id
116
+
117
+ def call(
118
+ self,
119
+ body: Any,
120
+ timeout: float = 5,
121
+ *,
122
+ priority: Optional[int] = None,
123
+ ) -> Any:
124
+ validate_priority(
125
+ priority,
126
+ 255,
127
+ )
128
+ if self.rpc_parameters is None:
129
+ raise RuntimeError('RPC requires connection parameters; obtain the queue through Connection.queue')
130
+ if isinstance(
131
+ timeout,
132
+ bool,
133
+ ) or not isinstance(
134
+ timeout,
135
+ (
136
+ int,
137
+ float,
138
+ ),
139
+ ) or not isfinite(timeout) or timeout <= 0:
140
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
141
+ deadline = monotonic() + timeout
142
+ settings = deepcopy(self.rpc_parameters)
143
+ settings.connection_attempts = 1
144
+ settings.socket_timeout = min(
145
+ settings.socket_timeout,
146
+ timeout,
147
+ )
148
+ settings.stack_timeout = timeout
149
+ settings.blocked_connection_timeout = timeout
150
+ connection = BlockingConnection(settings)
151
+ try:
152
+ channel = connection.channel()
153
+ reply_queue = channel.queue_declare(
154
+ queue='',
155
+ exclusive=True,
156
+ auto_delete=True,
157
+ ).method.queue
158
+ channel.confirm_delivery()
159
+ correlation_id = uuid4().hex
160
+ responses = []
161
+
162
+ def receive(
163
+ channel,
164
+ method,
165
+ properties,
166
+ payload,
167
+ ):
168
+ if properties.correlation_id == correlation_id:
169
+ responses.append((
170
+ properties,
171
+ payload,
172
+ ))
173
+
174
+ channel.basic_consume(
175
+ reply_queue,
176
+ on_message_callback=receive,
177
+ auto_ack=True,
178
+ )
179
+ remaining = deadline - monotonic()
180
+ if remaining <= 0:
181
+ raise TimeoutError('RPC timed out before request publication')
182
+ channel.basic_publish(
183
+ exchange='',
184
+ routing_key=self.queue,
185
+ body=self.encode(body),
186
+ properties=BasicProperties(
187
+ priority=priority,
188
+ reply_to=reply_queue,
189
+ correlation_id=correlation_id,
190
+ content_type=self.encode.format,
191
+ content_encoding=self.encode.charset,
192
+ expiration=str(max(
193
+ 1,
194
+ ceil(remaining * 1000),
195
+ )),
196
+ ),
197
+ mandatory=True,
198
+ )
199
+ while not responses:
200
+ remaining = deadline - monotonic()
201
+ if remaining <= 0:
202
+ raise TimeoutError(f'RPC response timed out for queue {self.queue!r}')
203
+ connection.process_data_events(time_limit=min(
204
+ remaining,
205
+ 0.1,
206
+ ))
207
+ properties, payload = responses[0]
208
+ response = self.decode(
209
+ payload,
210
+ properties.content_type,
211
+ properties.content_encoding,
212
+ )
213
+ if not isinstance(
214
+ response,
215
+ dict,
216
+ ) or type(response.get('ok')) is not bool:
217
+ raise ValueError('Invalid RPC response envelope')
218
+ if response['ok']:
219
+ if 'result' not in response:
220
+ raise ValueError('RPC response has no result')
221
+ return response['result']
222
+ error = response.get('error')
223
+ if not isinstance(
224
+ error,
225
+ dict,
226
+ ) or not isinstance(
227
+ error.get('code'),
228
+ str,
229
+ ) or not isinstance(
230
+ error.get('message'),
231
+ str,
232
+ ):
233
+ raise ValueError('Invalid RPC error response')
234
+ raise RemoteError(
235
+ error['code'],
236
+ error['message'],
237
+ )
238
+ finally:
239
+ if connection.is_open:
240
+ connection.close()
241
+
242
+ def pop(self, timeout: int = None) -> Optional[Event]:
243
+ channel = self.channel
244
+ channel.basic_qos(0, 1, False)
245
+ deadline = time() + (timeout / 1000) if timeout else None
246
+ while True:
247
+ method, properties, body = channel.basic_get(queue=self.queue, auto_ack=False)
248
+ if method:
249
+ return Event(
250
+ channel,
251
+ self.queue,
252
+ None,
253
+ method.delivery_tag,
254
+ properties,
255
+ self.decode(body, properties.content_type, properties.content_encoding),
256
+ )
257
+ if deadline and time() >= deadline:
258
+ break
259
+ sleep(0.1)
260
+ return
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABCMeta,
5
+ abstractmethod,
6
+ )
7
+
8
+ from json import (
9
+ dumps,
10
+ loads,
11
+ )
12
+ from typing import Any
13
+
14
+ __all__ = (
15
+ 'Encoder',
16
+ 'Decoder',
17
+ 'JavaScriptObjectNotationEncoder',
18
+ 'JavaScriptObjectNotationDecoder',
19
+ )
20
+
21
+ class Encoder(metaclass=ABCMeta):
22
+ """Encoder Interface"""
23
+ @abstractmethod
24
+ def __call__(self, obj: Any) -> bytes:
25
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
26
+
27
+ @property
28
+ @abstractmethod
29
+ def format(self) -> str:
30
+ raise NotImplementedError('{} must be implemented format'.format(self.__class__.__name__))
31
+
32
+ @property
33
+ @abstractmethod
34
+ def charset(self) -> str:
35
+ raise NotImplementedError('{} must be implemented charset'.format(self.__class__.__name__))
36
+
37
+
38
+ class Decoder(metaclass=ABCMeta):
39
+ """Decoder Interface"""
40
+ @abstractmethod
41
+ def __call__(self, obj: bytes, format: str, charset: str) -> Any:
42
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
43
+
44
+
45
+ class JavaScriptObjectNotationEncoder(Encoder):
46
+ """JavaScriptObjectNotation Encoder"""
47
+ def __call__(self, obj: Any) -> bytes:
48
+ return dumps(obj, ensure_ascii=False).encode(self.charset)
49
+
50
+ @property
51
+ def format(self) -> str: return 'application/json'
52
+
53
+ @property
54
+ def charset(self) -> str: return 'utf-8'
55
+
56
+
57
+ class JavaScriptObjectNotationDecoder(Decoder):
58
+ """JavaScriptObjectNotation Decoder"""
59
+ def __call__(self, obj: bytes, format: str, charset: str) -> Any:
60
+ if format != 'application/json': raise ValueError('format is not support')
61
+ return loads(obj.decode(charset))
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Readable,
6
+ )
7
+ from .Serializer import (
8
+ Encoder,
9
+ Decoder,
10
+ )
11
+ from .Event import Event
12
+
13
+ from pika import (
14
+ BlockingConnection,
15
+ BasicProperties,
16
+ )
17
+
18
+ from time import time
19
+ from uuid import uuid4
20
+
21
+ from typing import (
22
+ Dict,
23
+ Iterable,
24
+ )
25
+
26
+ __all__ = (
27
+ 'Queue'
28
+ )
29
+
30
+
31
+ class Stream(
32
+ Sendable,
33
+ Readable,
34
+ ):
35
+ """Stream of Event Broker for RabbitMQ"""
36
+ def __init__(
37
+ self,
38
+ connection: BlockingConnection,
39
+ encode: Encoder,
40
+ decode: Decoder,
41
+ name: str = None,
42
+ ):
43
+ self.connection = connection
44
+ self.encode = encode
45
+ self.decode = decode
46
+ self.channel = self.connection.channel()
47
+ self.channel.auto_decode = False
48
+ self.queue = name
49
+ self.event = None
50
+ return
51
+
52
+ def __del__(self):
53
+ if self.channel and self.channel.is_open:
54
+ self.channel.close()
55
+ return
56
+
57
+ def __str__(self): return self.queue
58
+
59
+ def send(
60
+ self,
61
+ body,
62
+ event: str = None,
63
+ headers: Dict = {},
64
+ timestamp: int = None,
65
+ id: str = None,
66
+ ):
67
+ if not timestamp: timestamp = int(time())
68
+ if not id: id = uuid4().hex
69
+ properties = BasicProperties(
70
+ type=event if event else '',
71
+ headers=headers,
72
+ content_type=self.encode.format,
73
+ content_encoding=self.encode.charset,
74
+ timestamp=timestamp,
75
+ message_id=id,
76
+ )
77
+ self.channel.basic_publish(
78
+ exchange='',
79
+ routing_key=self.queue,
80
+ properties=properties,
81
+ body=self.encode(body)
82
+ )
83
+ return id
84
+
85
+ def read(self, offset: int = 0, timeout: int = None) -> Iterable[Event]:
86
+ channel = self.channel
87
+ channel.basic_qos(0, 1, False)
88
+
89
+ deadline = time() + (timeout / 1000) if timeout else None
90
+
91
+ try:
92
+ for method, properties, body in channel.consume(
93
+ queue=self.queue,
94
+ auto_ack=False,
95
+ inactivity_timeout=1,
96
+ arguments={'x-stream-offset': offset},
97
+ ):
98
+ if method is None:
99
+ if deadline and time() >= deadline:
100
+ break
101
+ continue
102
+
103
+ event = Event(
104
+ channel,
105
+ self.queue,
106
+ method.consumer_tag,
107
+ method.delivery_tag,
108
+ properties,
109
+ self.decode(body, properties.content_type, properties.content_encoding),
110
+ )
111
+
112
+ try:
113
+ yield event
114
+ channel.basic_ack(delivery_tag=method.delivery_tag)
115
+ except Exception:
116
+ channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
117
+ raise
118
+ finally:
119
+ channel.cancel()
@@ -0,0 +1,74 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass
3
+ from typing import (
4
+ Any,
5
+ Optional,
6
+ )
7
+ from DefStream.Broker import validate_priority
8
+
9
+ __all__ = (
10
+ 'Subscription',
11
+ )
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Subscription:
16
+ queue: str
17
+ offset: Optional[int] = None
18
+ qos: Optional[int] = None
19
+ error: Optional[str] = None
20
+ max_priority: Optional[int] = None
21
+ priority: Optional[int] = None
22
+
23
+ def __post_init__(self):
24
+ if self.priority is not None and self.max_priority is None:
25
+ object.__setattr__(
26
+ self,
27
+ 'max_priority',
28
+ 255,
29
+ )
30
+ validate_priority(
31
+ self.max_priority,
32
+ 255,
33
+ minimum=1,
34
+ name='max_priority',
35
+ )
36
+ self.validate_priority(self.priority)
37
+ if not isinstance(self.queue, str) or not self.queue:
38
+ raise ValueError('Subscription queue must be a non-empty string')
39
+ if self.error is not None:
40
+ if not isinstance(
41
+ self.error,
42
+ str,
43
+ ) or not self.error:
44
+ raise ValueError('Subscription error must be a non-empty string or None')
45
+ if self.offset is not None and (isinstance(self.offset, bool) or not isinstance(self.offset, int)):
46
+ raise ValueError('Subscription offset must be an integer')
47
+ if self.qos is not None:
48
+ if isinstance(
49
+ self.qos,
50
+ bool,
51
+ ) or not isinstance(
52
+ self.qos,
53
+ int,
54
+ ) or not 0 <= self.qos <= 65535:
55
+ raise ValueError('Subscription qos must be an integer between 0 and 65535')
56
+ return
57
+
58
+ def validate_priority(
59
+ self,
60
+ priority: Optional[int],
61
+ ) -> None:
62
+ validate_priority(
63
+ priority,
64
+ 255 if self.max_priority is None else self.max_priority,
65
+ )
66
+ return
67
+
68
+ @classmethod
69
+ def parse(cls, parameters: Any) -> 'Subscription':
70
+ if isinstance(parameters, cls):
71
+ return parameters
72
+ if isinstance(parameters, Mapping):
73
+ return cls(**dict(parameters))
74
+ raise TypeError('RabbitMQ subscriptions require a Subscription or mapping')
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .Configuration import Configuration
4
+ from .Connection import (
5
+ Connection,
6
+ ExchangeType,
7
+ Parameters,
8
+ All,
9
+ Any,
10
+ )
11
+ from .Exchange import Exchange
12
+ from .Queue import Queue
13
+ from .Stream import Stream
14
+ from .Consumer import Consumer
15
+ from .Subscription import Subscription
16
+ from .Policies import (
17
+ Requeue,
18
+ Reject,
19
+ )
20
+ from .Event import (
21
+ Event,
22
+ EventHandler,
23
+ )
24
+ from .Serializer import (
25
+ Encoder,
26
+ Decoder,
27
+ )
28
+
29
+ __all__ = (
30
+ 'Configuration',
31
+ 'Connection',
32
+ 'ExchangeType',
33
+ 'Parameters',
34
+ 'All',
35
+ 'Any',
36
+ 'Exchange',
37
+ 'Queue',
38
+ 'Stream',
39
+ 'Consumer',
40
+ 'Subscription',
41
+ 'Requeue',
42
+ 'Reject',
43
+ 'Event',
44
+ 'EventHandler',
45
+ 'Encoder',
46
+ 'Decoder',
47
+ )