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