DefStream 0.1.20__81-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.
Files changed (73) hide show
  1. DefStream/Broker/Configuration.py +17 -0
  2. DefStream/Broker/Connection.py +141 -0
  3. DefStream/Broker/Consumer.py +59 -0
  4. DefStream/Broker/Errors.py +14 -0
  5. DefStream/Broker/Event.py +35 -0
  6. DefStream/Broker/Implements/RabbitMQ/Configuration.py +49 -0
  7. DefStream/Broker/Implements/RabbitMQ/Connection.py +549 -0
  8. DefStream/Broker/Implements/RabbitMQ/Consumer.py +360 -0
  9. DefStream/Broker/Implements/RabbitMQ/Event.py +146 -0
  10. DefStream/Broker/Implements/RabbitMQ/Exchange.py +277 -0
  11. DefStream/Broker/Implements/RabbitMQ/Queue.py +321 -0
  12. DefStream/Broker/Implements/RabbitMQ/Serializer.py +75 -0
  13. DefStream/Broker/Implements/RabbitMQ/Stream.py +181 -0
  14. DefStream/Broker/Implements/RabbitMQ/Subscription.py +90 -0
  15. DefStream/Broker/Implements/RabbitMQ/__init__.py +45 -0
  16. DefStream/Broker/Implements/Redis/Configuration.py +59 -0
  17. DefStream/Broker/Implements/Redis/Connection.py +265 -0
  18. DefStream/Broker/Implements/Redis/Consumer.py +368 -0
  19. DefStream/Broker/Implements/Redis/Event.py +212 -0
  20. DefStream/Broker/Implements/Redis/Queue.py +284 -0
  21. DefStream/Broker/Implements/Redis/Serializer.py +48 -0
  22. DefStream/Broker/Implements/Redis/Subscription.py +46 -0
  23. DefStream/Broker/Implements/Redis/__init__.py +29 -0
  24. DefStream/Broker/Implements/__init__.py +1 -0
  25. DefStream/Broker/Priority.py +26 -0
  26. DefStream/Broker/Protocol.py +45 -0
  27. DefStream/Broker/__init__.py +53 -0
  28. DefStream/Descriptor/DataObject.py +409 -0
  29. DefStream/Descriptor/Types.py +248 -0
  30. DefStream/Descriptor/Value.py +188 -0
  31. DefStream/Descriptor/__init__.py +172 -0
  32. DefStream/Inspector/Extension.py +69 -0
  33. DefStream/Inspector/Extensions/Metrics.py +316 -0
  34. DefStream/Inspector/Extensions/Ping.py +107 -0
  35. DefStream/Inspector/Extensions/Play.html +373 -0
  36. DefStream/Inspector/Extensions/Play.py +415 -0
  37. DefStream/Inspector/Extensions/Specifications.py +176 -0
  38. DefStream/Inspector/Extensions/Status.html +206 -0
  39. DefStream/Inspector/Extensions/Status.py +124 -0
  40. DefStream/Inspector/Extensions/__init__.py +13 -0
  41. DefStream/Inspector/Inspector.py +354 -0
  42. DefStream/Inspector/Response.py +14 -0
  43. DefStream/Inspector/__init__.py +9 -0
  44. DefStream/Validator/Pattern.py +36 -0
  45. DefStream/Validator/Patterns/Array.py +30 -0
  46. DefStream/Validator/Patterns/Common.py +92 -0
  47. DefStream/Validator/Patterns/Compare.py +180 -0
  48. DefStream/Validator/Patterns/Condition.py +60 -0
  49. DefStream/Validator/Patterns/DataObject.py +93 -0
  50. DefStream/Validator/Patterns/DateTime.py +111 -0
  51. DefStream/Validator/Patterns/Number.py +43 -0
  52. DefStream/Validator/Patterns/Object.py +158 -0
  53. DefStream/Validator/Patterns/Size.py +110 -0
  54. DefStream/Validator/Patterns/String.py +92 -0
  55. DefStream/Validator/Patterns/Type.py +355 -0
  56. DefStream/Validator/Patterns/__init__.py +137 -0
  57. DefStream/Validator/Validate.py +29 -0
  58. DefStream/Validator/Validator.py +30 -0
  59. DefStream/Validator/__init__.py +11 -0
  60. DefStream/Worker/Executor.py +371 -0
  61. DefStream/Worker/Message.py +180 -0
  62. DefStream/Worker/Pool.py +245 -0
  63. DefStream/Worker/Run.py +357 -0
  64. DefStream/Worker/Specification.py +87 -0
  65. DefStream/Worker/Startup.py +174 -0
  66. DefStream/Worker/Worker.py +421 -0
  67. DefStream/Worker/__init__.py +47 -0
  68. DefStream/__init__.py +0 -0
  69. defstream-0.1.20.dist-info/METADATA +92 -0
  70. defstream-0.1.20.dist-info/RECORD +73 -0
  71. defstream-0.1.20.dist-info/WHEEL +6 -0
  72. defstream-0.1.20.dist-info/licenses/LICENSE +674 -0
  73. defstream-0.1.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,368 @@
1
+ from collections import deque
2
+ from logging import getLogger
3
+ from queue import (
4
+ Empty,
5
+ SimpleQueue,
6
+ )
7
+ from threading import Event as Wakeup
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Dict,
12
+ Optional,
13
+ Tuple,
14
+ Union,
15
+ )
16
+ from uuid import uuid4
17
+
18
+ from redis import Redis
19
+ from redis.cluster import RedisCluster
20
+
21
+ from DefStream.Broker import (
22
+ Consumer as BaseConsumer,
23
+ Event as BaseEvent,
24
+ EventHandler,
25
+ )
26
+ from .Event import (
27
+ Event,
28
+ Requeue,
29
+ )
30
+ from .Queue import Queue
31
+ from .Serializer import (
32
+ Decoder,
33
+ Encoder,
34
+ )
35
+ from .Subscription import Subscription
36
+ from DefStream.Broker.Event import EventErrorPolicy
37
+
38
+ __all__ = (
39
+ 'Consumer',
40
+ )
41
+
42
+
43
+ class Consumer(BaseConsumer):
44
+ logger = getLogger('Broker.Redis.Consumer')
45
+
46
+ def __init__(
47
+ self,
48
+ connection: Union[Redis, RedisCluster],
49
+ encode: Encoder,
50
+ decode: Decoder,
51
+ handler: Optional[EventHandler] = None,
52
+ qos: int = 1,
53
+ reclaim_idle: Optional[int] = 60000,
54
+ ):
55
+ if isinstance(
56
+ qos,
57
+ bool,
58
+ ) or not isinstance(
59
+ qos,
60
+ int,
61
+ ) or qos < 1:
62
+ raise ValueError('Redis qos must be a positive integer')
63
+ if reclaim_idle is not None and reclaim_idle < 1:
64
+ raise ValueError('Redis reclaim_idle must be positive milliseconds or None')
65
+ self.connection = connection
66
+ self.encode = encode
67
+ self.decode = decode
68
+ self.handler = handler
69
+ self.qos = qos
70
+ self.reclaim_idle = reclaim_idle
71
+ self.name = uuid4().hex
72
+ self.subscriptions: Dict[str, Tuple[Queue, EventHandler]] = {}
73
+ self.rotation = deque()
74
+ self.pending: Dict[Tuple[str, str], Event] = {}
75
+ self.cursors: Dict[str, str] = {}
76
+ self.callbacks: SimpleQueue[Callable[[], None]] = SimpleQueue()
77
+ self.wakeup = Wakeup()
78
+ self.stopped = False
79
+ self.closed = False
80
+ self.logger.debug(
81
+ 'Consumer created: consumer=%s qos=%s reclaim_idle=%sms',
82
+ self.name,
83
+ self.qos,
84
+ self.reclaim_idle,
85
+ )
86
+
87
+ def subs(
88
+ self,
89
+ queue: Any,
90
+ handler: Optional[EventHandler] = None,
91
+ ) -> None:
92
+ if self.closed:
93
+ raise RuntimeError('Redis consumer is closed')
94
+ subscription = Subscription.parse(queue)
95
+ handler = handler if handler is not None else self.handler
96
+ if handler is None:
97
+ raise ValueError('subs requires a handler or a default handler')
98
+ if subscription.queue in self.subscriptions:
99
+ raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
100
+ self.logger.debug(
101
+ 'Preparing Consumer subscription: queue=%s group=%s consumer=%s',
102
+ subscription.queue,
103
+ subscription.group,
104
+ self.name,
105
+ )
106
+ stream = Queue(
107
+ self.connection,
108
+ self.encode,
109
+ self.decode,
110
+ subscription.queue,
111
+ subscription.group,
112
+ )
113
+ stream.create_group()
114
+ self.subscriptions[subscription.queue] = (
115
+ stream,
116
+ handler,
117
+ )
118
+ self.cursors[subscription.queue] = '0-0'
119
+ self.rotation.append(subscription.queue)
120
+ self.logger.info(
121
+ 'Consumer subscribed: queue=%s group=%s consumer=%s qos=%s reclaim_idle=%sms',
122
+ subscription.queue,
123
+ subscription.group,
124
+ self.name,
125
+ self.qos,
126
+ self.reclaim_idle,
127
+ )
128
+
129
+ def call(
130
+ self,
131
+ callback: Callable[[], None],
132
+ ) -> None:
133
+ if self.closed:
134
+ raise RuntimeError('Redis consumer is closed')
135
+ self.callbacks.put(callback)
136
+ self.wakeup.set()
137
+ self.logger.debug(
138
+ 'Consumer callback scheduled: consumer=%s',
139
+ self.name,
140
+ )
141
+
142
+ def fail(
143
+ self,
144
+ event: BaseEvent,
145
+ policy: Optional[EventErrorPolicy] = None,
146
+ ) -> None:
147
+ self.logger.debug(
148
+ 'Applying message failure policy: consumer=%s policy=%s',
149
+ self.name,
150
+ type(policy).__name__ if policy is not None else 'Requeue',
151
+ )
152
+ (policy if policy is not None else Requeue())(event)
153
+
154
+ def drain(self) -> None:
155
+ while True:
156
+ try:
157
+ callback = self.callbacks.get_nowait()
158
+ except Empty:
159
+ break
160
+ self.logger.debug(
161
+ 'Executing Consumer callback: consumer=%s',
162
+ self.name,
163
+ )
164
+ try:
165
+ callback()
166
+ except Exception as error:
167
+ self.logger.error(
168
+ 'Consumer callback failed: consumer=%s error_type=%s',
169
+ self.name,
170
+ type(error).__name__,
171
+ )
172
+ raise
173
+ pending_count = len(self.pending)
174
+ self.pending = {
175
+ key: event
176
+ for key, event in self.pending.items()
177
+ if not event.settled
178
+ }
179
+ settled_count = pending_count - len(self.pending)
180
+ if settled_count:
181
+ self.logger.debug(
182
+ 'Settled Events removed: consumer=%s removed=%s pending=%s',
183
+ self.name,
184
+ settled_count,
185
+ len(self.pending),
186
+ )
187
+
188
+ def deliver(
189
+ self,
190
+ stream: Queue,
191
+ handler: EventHandler,
192
+ entries: list,
193
+ ) -> None:
194
+ for id, fields in entries:
195
+ key = (
196
+ stream.queue,
197
+ id.decode('ascii'),
198
+ )
199
+ if key in self.pending:
200
+ self.logger.debug(
201
+ 'Skipping delivery of already pending message: queue=%s group=%s id=%s',
202
+ stream.queue,
203
+ stream.group,
204
+ key[1],
205
+ )
206
+ continue
207
+ event = stream.event(
208
+ id,
209
+ fields,
210
+ stream.group,
211
+ self.name,
212
+ )
213
+ self.pending[key] = event
214
+ self.logger.debug(
215
+ 'Delivering message: queue=%s group=%s consumer=%s id=%s pending=%s',
216
+ stream.queue,
217
+ stream.group,
218
+ self.name,
219
+ event.id,
220
+ len(self.pending),
221
+ )
222
+ try:
223
+ handler(event)
224
+ except Exception as error:
225
+ self.logger.error(
226
+ 'Message handler dispatch failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
227
+ stream.queue,
228
+ stream.group,
229
+ self.name,
230
+ event.id,
231
+ type(error).__name__,
232
+ )
233
+ raise
234
+ self.drain()
235
+
236
+ def run(self) -> None:
237
+ if self.closed:
238
+ raise RuntimeError('Redis consumer is closed')
239
+ if not self.subscriptions:
240
+ raise RuntimeError('Subscribe before running the Redis consumer')
241
+ self.logger.info(
242
+ 'Starting Consumer consumption loop: consumer=%s subscriptions=%s qos=%s reclaim_idle=%sms',
243
+ self.name,
244
+ len(self.subscriptions),
245
+ self.qos,
246
+ self.reclaim_idle,
247
+ )
248
+ qos_limited = False
249
+ while not self.stopped:
250
+ self.wakeup.clear()
251
+ self.drain()
252
+ for _ in range(len(self.rotation)):
253
+ available = self.qos - len(self.pending)
254
+ if available <= 0:
255
+ if not qos_limited:
256
+ self.logger.debug(
257
+ 'Deferring reads at QoS limit: consumer=%s qos=%s pending=%s',
258
+ self.name,
259
+ self.qos,
260
+ len(self.pending),
261
+ )
262
+ qos_limited = True
263
+ break
264
+ qos_limited = False
265
+ if self.stopped:
266
+ break
267
+ name = self.rotation.popleft()
268
+ self.rotation.append(name)
269
+ stream, handler = self.subscriptions[name]
270
+ if self.reclaim_idle is not None:
271
+ old_cursor = self.cursors[stream.queue]
272
+ try:
273
+ claimed = self.connection.xautoclaim(
274
+ stream.queue,
275
+ stream.group,
276
+ self.name,
277
+ self.reclaim_idle,
278
+ start_id=old_cursor,
279
+ count=available,
280
+ )
281
+ except Exception as error:
282
+ self.logger.error(
283
+ 'Pending message reclaim failed: queue=%s group=%s consumer=%s error_type=%s',
284
+ stream.queue,
285
+ stream.group,
286
+ self.name,
287
+ type(error).__name__,
288
+ )
289
+ raise
290
+ self.cursors[stream.queue] = claimed[0].decode('ascii')
291
+ if claimed[1] or self.cursors[stream.queue] != old_cursor:
292
+ self.logger.debug(
293
+ 'Pending message reclaim result: queue=%s group=%s consumer=%s count=%s cursor=%s',
294
+ stream.queue,
295
+ stream.group,
296
+ self.name,
297
+ len(claimed[1]),
298
+ self.cursors[stream.queue],
299
+ )
300
+ self.deliver(
301
+ stream,
302
+ handler,
303
+ claimed[1],
304
+ )
305
+ available = self.qos - len(self.pending)
306
+ if available <= 0:
307
+ if not qos_limited:
308
+ self.logger.debug(
309
+ 'Deferring reads at QoS limit: consumer=%s qos=%s pending=%s',
310
+ self.name,
311
+ self.qos,
312
+ len(self.pending),
313
+ )
314
+ qos_limited = True
315
+ continue
316
+ qos_limited = False
317
+ if self.stopped:
318
+ continue
319
+ try:
320
+ messages = self.connection.xreadgroup(
321
+ stream.group,
322
+ self.name,
323
+ {stream.queue: '>'},
324
+ count=available,
325
+ )
326
+ except Exception as error:
327
+ self.logger.error(
328
+ 'New message read failed: queue=%s group=%s consumer=%s error_type=%s',
329
+ stream.queue,
330
+ stream.group,
331
+ self.name,
332
+ type(error).__name__,
333
+ )
334
+ raise
335
+ for _, entries in messages:
336
+ if not isinstance(entries, list):
337
+ raise TypeError('Redis Stream read returned invalid entries')
338
+ self.deliver(
339
+ stream,
340
+ handler,
341
+ entries,
342
+ )
343
+ self.wakeup.wait(0.05)
344
+ self.logger.info(
345
+ 'Consumer consumption loop ended: consumer=%s pending=%s',
346
+ self.name,
347
+ len(self.pending),
348
+ )
349
+
350
+ def stop(self) -> None:
351
+ if not self.stopped:
352
+ self.logger.info(
353
+ 'Consumer stop requested: consumer=%s pending=%s',
354
+ self.name,
355
+ len(self.pending),
356
+ )
357
+ self.stopped = True
358
+ self.wakeup.set()
359
+
360
+ def close(self) -> None:
361
+ if not self.closed:
362
+ self.logger.info(
363
+ 'Consumer closed: consumer=%s pending=%s',
364
+ self.name,
365
+ len(self.pending),
366
+ )
367
+ self.stop()
368
+ self.closed = True
@@ -0,0 +1,212 @@
1
+ from logging import getLogger
2
+ from typing import (
3
+ Any,
4
+ Optional,
5
+ Union,
6
+ )
7
+
8
+ from redis import Redis
9
+ from redis.cluster import RedisCluster
10
+
11
+ from DefStream.Broker.Event import (
12
+ Event as BaseEvent,
13
+ EventErrorPolicy,
14
+ )
15
+
16
+ __all__ = (
17
+ 'Event',
18
+ 'Requeue',
19
+ 'Reject',
20
+ )
21
+
22
+
23
+ class Event(BaseEvent):
24
+ logger = getLogger('Broker.Redis.Event')
25
+
26
+ def __init__(
27
+ self,
28
+ connection: Union[Redis, RedisCluster],
29
+ queue: str,
30
+ id: str,
31
+ body: Any,
32
+ group: Optional[str] = None,
33
+ consumer: Optional[str] = None,
34
+ ):
35
+ self.connection = connection
36
+ self.queue = queue
37
+ self.id = id
38
+ self.payload = body
39
+ self.group = group
40
+ self.consumer = consumer
41
+ self.settled = False
42
+
43
+ @property
44
+ def body(self) -> Any:
45
+ return self.payload
46
+
47
+ @body.setter
48
+ def body(
49
+ self,
50
+ body: Any,
51
+ ) -> None:
52
+ self.payload = body
53
+
54
+ @property
55
+ def src(self) -> str:
56
+ return self.queue
57
+
58
+ def ack(self) -> None:
59
+ if self.group is None:
60
+ self.logger.warning(
61
+ 'Rejected ACK for history Event without a consumer group: queue=%s id=%s',
62
+ self.queue,
63
+ self.id,
64
+ )
65
+ raise RuntimeError('Events from read() have no consumer group to acknowledge')
66
+ if not self.settled:
67
+ self.logger.debug(
68
+ 'Attempting Event ACK: queue=%s group=%s consumer=%s id=%s',
69
+ self.queue,
70
+ self.group,
71
+ self.consumer,
72
+ self.id,
73
+ )
74
+ try:
75
+ result = self.connection.xack(
76
+ self.queue,
77
+ self.group,
78
+ self.id,
79
+ )
80
+ except Exception as error:
81
+ self.logger.error(
82
+ 'Event ACK failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
83
+ self.queue,
84
+ self.group,
85
+ self.consumer,
86
+ self.id,
87
+ type(error).__name__,
88
+ )
89
+ raise
90
+ self.settled = True
91
+ if isinstance(result, int) and result > 0:
92
+ self.logger.debug(
93
+ 'Event ACK completed: queue=%s group=%s consumer=%s id=%s count=%s',
94
+ self.queue,
95
+ self.group,
96
+ self.consumer,
97
+ self.id,
98
+ result,
99
+ )
100
+ else:
101
+ self.logger.debug(
102
+ 'No Event ACK target: queue=%s group=%s consumer=%s id=%s count=%s',
103
+ self.queue,
104
+ self.group,
105
+ self.consumer,
106
+ self.id,
107
+ result,
108
+ )
109
+ else:
110
+ self.logger.debug(
111
+ 'Skipping ACK for settled Event: queue=%s group=%s consumer=%s id=%s',
112
+ self.queue,
113
+ self.group,
114
+ self.consumer,
115
+ self.id,
116
+ )
117
+
118
+ def nack(self) -> None:
119
+ if self.group is None:
120
+ self.logger.warning(
121
+ 'Rejected NACK for history Event without a consumer group: queue=%s id=%s',
122
+ self.queue,
123
+ self.id,
124
+ )
125
+ raise RuntimeError('Events from read() have no consumer group to requeue')
126
+ if not self.settled:
127
+ self.logger.debug(
128
+ 'Attempting Event requeue: queue=%s group=%s consumer=%s id=%s',
129
+ self.queue,
130
+ self.group,
131
+ self.consumer,
132
+ self.id,
133
+ )
134
+ # Requeue and acknowledge atomically on one stream (also valid in Cluster).
135
+ try:
136
+ result = self.connection.eval(
137
+ """
138
+ local pending = redis.call('XPENDING', KEYS[1], ARGV[1], ARGV[2], ARGV[2], 1)
139
+ if #pending == 0 then return 0 end
140
+ local entries = redis.call('XRANGE', KEYS[1], ARGV[2], ARGV[2])
141
+ if #entries == 0 then return redis.error_reply('Pending entry was deleted') end
142
+ redis.call('XADD', KEYS[1], '*', unpack(entries[1][2]))
143
+ return redis.call('XACK', KEYS[1], ARGV[1], ARGV[2])
144
+ """,
145
+ 1,
146
+ self.queue,
147
+ self.group,
148
+ self.id,
149
+ )
150
+ except Exception as error:
151
+ self.logger.error(
152
+ 'Event atomic requeue failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
153
+ self.queue,
154
+ self.group,
155
+ self.consumer,
156
+ self.id,
157
+ type(error).__name__,
158
+ )
159
+ raise
160
+ self.settled = True
161
+ if isinstance(result, int) and result > 0:
162
+ self.logger.debug(
163
+ 'Event requeue and ACK completed: queue=%s group=%s consumer=%s id=%s count=%s',
164
+ self.queue,
165
+ self.group,
166
+ self.consumer,
167
+ self.id,
168
+ result,
169
+ )
170
+ else:
171
+ self.logger.debug(
172
+ 'No Event requeue target: queue=%s group=%s consumer=%s id=%s count=%s',
173
+ self.queue,
174
+ self.group,
175
+ self.consumer,
176
+ self.id,
177
+ result,
178
+ )
179
+ else:
180
+ self.logger.debug(
181
+ 'Skipping NACK for settled Event: queue=%s group=%s consumer=%s id=%s',
182
+ self.queue,
183
+ self.group,
184
+ self.consumer,
185
+ self.id,
186
+ )
187
+
188
+ def reject(self) -> None:
189
+ self.logger.debug(
190
+ 'Event reject requested (currently implemented as ACK): queue=%s group=%s consumer=%s id=%s',
191
+ self.queue,
192
+ self.group,
193
+ self.consumer,
194
+ self.id,
195
+ )
196
+ self.ack()
197
+
198
+
199
+ class Requeue(EventErrorPolicy):
200
+ def __call__(
201
+ self,
202
+ event: BaseEvent,
203
+ ) -> None:
204
+ event.nack()
205
+
206
+
207
+ class Reject(EventErrorPolicy):
208
+ def __call__(
209
+ self,
210
+ event: BaseEvent,
211
+ ) -> None:
212
+ event.reject()