DefStream 0.1.22__90-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 +550 -0
  8. DefStream/Broker/Implements/RabbitMQ/Consumer.py +416 -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 +54 -0
  27. DefStream/Broker/__init__.py +55 -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 +525 -0
  61. DefStream/Worker/Message.py +180 -0
  62. DefStream/Worker/Pool.py +282 -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 +571 -0
  67. DefStream/Worker/__init__.py +47 -0
  68. DefStream/__init__.py +0 -0
  69. defstream-0.1.22.dist-info/METADATA +92 -0
  70. defstream-0.1.22.dist-info/RECORD +73 -0
  71. defstream-0.1.22.dist-info/WHEEL +6 -0
  72. defstream-0.1.22.dist-info/licenses/LICENSE +674 -0
  73. defstream-0.1.22.dist-info/top_level.txt +1 -0
@@ -0,0 +1,571 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from collections.abc import Mapping
6
+ from logging import getLogger
7
+ from math import isfinite
8
+ from threading import (
9
+ Event as PyEvent,
10
+ RLock,
11
+ )
12
+ from typing import (
13
+ Any,
14
+ List,
15
+ Callable,
16
+ Optional,
17
+ Sequence,
18
+ Type,
19
+ Tuple,
20
+ Union,
21
+ Dict,
22
+ )
23
+
24
+ from ..Broker import (
25
+ Connection,
26
+ GetConsumer,
27
+ Consumer,
28
+ Recoverable,
29
+ EventHandler,
30
+ )
31
+ from ..Broker.Event import Event
32
+ from .Message import task_parameters
33
+ from ..Inspector import Inspector
34
+ from .Pool import (
35
+ Pool,
36
+ PoolFull,
37
+ )
38
+ from .Run import Run
39
+ from .Executor import run_cleanups
40
+ from .Startup import startup
41
+
42
+ __all__ = (
43
+ 'Worker',
44
+ 'WorkerSetup',
45
+ 'WorkerCleanup',
46
+ )
47
+
48
+ class WorkerSetup(ABC):
49
+ @abstractmethod
50
+ def __call__(
51
+ self,
52
+ worker: 'Worker',
53
+ ) -> None:
54
+ raise NotImplementedError
55
+
56
+
57
+ class WorkerCleanup(ABC):
58
+ @abstractmethod
59
+ def __call__(
60
+ self,
61
+ worker: 'Worker',
62
+ ) -> None:
63
+ raise NotImplementedError
64
+
65
+
66
+ def worker_hooks(
67
+ hooks: Optional[Union[Callable[['Worker'], None], Sequence[Callable[['Worker'], None]]]],
68
+ ) -> Tuple[Callable[['Worker'], None], ...]:
69
+ result = () if hooks is None else tuple(hooks) if isinstance(
70
+ hooks,
71
+ Sequence,
72
+ ) else (hooks,)
73
+ if not all(callable(hook) for hook in result):
74
+ raise TypeError('Worker hooks must be callable')
75
+ return result
76
+
77
+
78
+ class Worker:
79
+ logger = getLogger('Worker')
80
+
81
+ def __init__(
82
+ self,
83
+ broker: Connection,
84
+ pool: Pool,
85
+ setups: Optional[Union[WorkerSetup, Sequence[WorkerSetup]]] = None,
86
+ cleanups: Optional[Union[WorkerCleanup, Sequence[WorkerCleanup]]] = None,
87
+ inspector: Optional[Inspector] = None,
88
+ *,
89
+ recovery_attempts: int = 5,
90
+ recovery_interval: float = 60,
91
+ ) -> None:
92
+ if isinstance(
93
+ recovery_attempts,
94
+ bool,
95
+ ) or not isinstance(
96
+ recovery_attempts,
97
+ int,
98
+ ):
99
+ raise TypeError('Recovery attempts must be an integer')
100
+ if recovery_attempts < 0:
101
+ raise ValueError('Recovery attempts must not be negative')
102
+ if isinstance(
103
+ recovery_interval,
104
+ bool,
105
+ ) or not isinstance(
106
+ recovery_interval,
107
+ (
108
+ int,
109
+ float,
110
+ ),
111
+ ):
112
+ raise TypeError('Recovery interval must be a number of seconds')
113
+ if not isfinite(recovery_interval) or recovery_interval < 0:
114
+ raise ValueError('Recovery interval must be finite and non-negative')
115
+ self.broker = broker
116
+ self.pool = pool
117
+ self.tasks: List[Type[Run]] = []
118
+ self.consumer: Optional[Consumer] = None
119
+ self.started = False
120
+ self.running = PyEvent()
121
+ self.stopping = PyEvent()
122
+ self.finished = PyEvent()
123
+ self.pending = 0
124
+ self.setups = worker_hooks(setups)
125
+ self.cleanups = worker_hooks(cleanups)
126
+ self.inspector = inspector
127
+ self.recovery_attempts = recovery_attempts
128
+ self.recovery_interval = recovery_interval
129
+ self.generation = 0
130
+ self.delivery_lock = RLock()
131
+ self.deliveries: Dict[object, Callable[[], None]] = {}
132
+ self.consumption_failed = False
133
+
134
+ def add(
135
+ self,
136
+ run: Type[Run],
137
+ ) -> None:
138
+ if self.started:
139
+ raise RuntimeError('Cannot register runs after starting the worker')
140
+ task_parameters(run)
141
+ if run in self.tasks:
142
+ raise ValueError('Run is already registered')
143
+ if any(
144
+ registered.name == run.name
145
+ for registered in self.tasks
146
+ ):
147
+ raise ValueError('Duplicate Run name: ' + run.name)
148
+ self.tasks.append(run)
149
+ self.logger.debug(
150
+ 'Task registered: task=%s.%s',
151
+ run.__module__,
152
+ run.__qualname__,
153
+ )
154
+
155
+ def run(self) -> None:
156
+ if self.started:
157
+ raise RuntimeError('Worker can only be started once')
158
+ if not self.tasks:
159
+ raise RuntimeError('Register at least one Run before starting')
160
+ self.started = True
161
+ self.logger.info(
162
+ 'Starting Worker: tasks=%s',
163
+ len(self.tasks),
164
+ )
165
+ try:
166
+ startup(self)
167
+ if self.inspector is not None:
168
+ self.inspector.start(self)
169
+ self.logger.debug('Connecting to broker')
170
+ self.broker.connect()
171
+ self.logger.debug('Broker connected')
172
+ for setup in self.setups:
173
+ self.logger.debug(
174
+ 'Starting Worker setup: hook=%s',
175
+ type(setup).__qualname__,
176
+ )
177
+ setup(self)
178
+ self.logger.debug(
179
+ 'Worker setup completed: hook=%s',
180
+ type(setup).__qualname__,
181
+ )
182
+ if not isinstance(self.broker, GetConsumer):
183
+ raise TypeError('Worker broker must support consumer creation')
184
+ self.subscribe()
185
+ if not self.stopping.is_set():
186
+ self.consume()
187
+ finally:
188
+ self.running.clear()
189
+ self.finished.set()
190
+ if self.pending:
191
+ self.logger.error(
192
+ 'Consumer exited with unconfirmed deliveries: pending=%s',
193
+ self.pending,
194
+ )
195
+ try:
196
+ self.logger.info(
197
+ 'Draining Worker tasks: pending=%s',
198
+ self.pending,
199
+ )
200
+ if self.consumption_failed:
201
+ self.pool.stop(abort=True)
202
+ else:
203
+ self.pool.stop()
204
+ finally:
205
+ try:
206
+ self.cleanup()
207
+ finally:
208
+ try:
209
+ if self.consumer is not None:
210
+ self.consumer.close()
211
+ finally:
212
+ try:
213
+ self.broker.close()
214
+ finally:
215
+ if self.inspector is not None and self.inspector.worker is self:
216
+ self.inspector.stop()
217
+ self.logger.info('Worker shutdown completed')
218
+
219
+ def subscribe(self) -> None:
220
+ if not isinstance(self.broker, GetConsumer):
221
+ raise TypeError('Worker broker must support consumer creation')
222
+ self.consumer = self.broker.consumer()
223
+ self.logger.debug('Consumer created')
224
+ for run in self.tasks:
225
+ self.logger.debug(
226
+ 'Subscribing task: task=%s.%s',
227
+ run.__module__,
228
+ run.__qualname__,
229
+ )
230
+ self.consumer.subs(
231
+ run.parameters,
232
+ Handler(
233
+ self,
234
+ run,
235
+ ),
236
+ )
237
+ self.logger.debug(
238
+ 'Task subscribed: task=%s.%s',
239
+ run.__module__,
240
+ run.__qualname__,
241
+ )
242
+ return
243
+
244
+ def discard_deliveries(self) -> None:
245
+ """Retire outstanding Inspector contexts before replacing an execution generation."""
246
+ with self.delivery_lock:
247
+ self.generation += 1
248
+ for discard in self.deliveries.values():
249
+ discard()
250
+ self.deliveries.clear()
251
+ self.pending = 0
252
+ return
253
+
254
+ def consume(self) -> None:
255
+ while not self.stopping.is_set():
256
+ consumer = self.consumer
257
+ if consumer is None:
258
+ raise RuntimeError('Worker consumer is not initialized')
259
+ self.running.set()
260
+ if self.stopping.is_set():
261
+ self.running.clear()
262
+ return
263
+ self.logger.info('Worker ready to consume')
264
+ try:
265
+ consumer.run()
266
+ return
267
+ except Exception as error:
268
+ self.running.clear()
269
+ if not isinstance(consumer, Recoverable):
270
+ raise
271
+ self.consumption_failed = True
272
+ self.discard_deliveries()
273
+ if not consumer.is_recoverable(error):
274
+ raise
275
+ if self.stopping.is_set():
276
+ return
277
+ self.recover(error, consumer)
278
+ self.consumption_failed = False
279
+ return
280
+
281
+ def recover(self, error: Exception, previous: Recoverable) -> None:
282
+ self.logger.warning(
283
+ 'Recovering Worker after consumption failure: error_type=%s attempts=%s',
284
+ type(error).__name__,
285
+ self.recovery_attempts,
286
+ )
287
+ if self.recovery_attempts == 0:
288
+ raise error
289
+ self.pool.restart()
290
+ for attempt in range(self.recovery_attempts):
291
+ if attempt and self.stopping.wait(self.recovery_interval):
292
+ return
293
+ if self.stopping.is_set():
294
+ return
295
+ try:
296
+ if self.consumer is not None:
297
+ try:
298
+ self.consumer.close()
299
+ except Exception as cleanup_error:
300
+ self.logger.warning(
301
+ 'Consumer cleanup failed during recovery: error_type=%s',
302
+ type(cleanup_error).__name__,
303
+ )
304
+ if not previous.is_recoverable(cleanup_error):
305
+ raise
306
+ try:
307
+ self.broker.close()
308
+ except Exception as cleanup_error:
309
+ self.logger.warning(
310
+ 'Broker cleanup failed during recovery: error_type=%s',
311
+ type(cleanup_error).__name__,
312
+ )
313
+ if not previous.is_recoverable(cleanup_error):
314
+ raise
315
+ self.broker.connect()
316
+ self.subscribe()
317
+ except Exception as failure:
318
+ self.logger.error(
319
+ 'Worker recovery attempt failed: attempt=%s error_type=%s',
320
+ attempt + 1,
321
+ type(failure).__name__,
322
+ )
323
+ if not previous.is_recoverable(failure) or attempt + 1 == self.recovery_attempts:
324
+ raise
325
+ else:
326
+ self.logger.info('Worker recovery completed: attempt=%s', attempt + 1)
327
+ return
328
+ return
329
+
330
+ def cleanup(self) -> None:
331
+ self.logger.debug(
332
+ 'Starting Worker cleanup: hooks=%s',
333
+ len(self.cleanups),
334
+ )
335
+ run_cleanups(
336
+ self.cleanups,
337
+ self,
338
+ )
339
+ self.logger.debug('Worker cleanup completed')
340
+
341
+ def stop(self) -> None:
342
+ if not self.stopping.is_set():
343
+ self.logger.info(
344
+ 'Worker shutdown requested: pending=%s',
345
+ self.pending,
346
+ )
347
+ self.stopping.set()
348
+ if self.consumer is not None and self.running.is_set() and not self.finished.is_set():
349
+ self.consumer.call(self.try_stop)
350
+
351
+ def try_stop(self) -> None:
352
+ if self.stopping.is_set() and self.pending == 0 and self.consumer is not None:
353
+ self.logger.debug('No pending tasks; requesting Consumer stop')
354
+ self.consumer.stop()
355
+
356
+ def receive(
357
+ self,
358
+ run: Type[Run],
359
+ event: Event,
360
+ ) -> None:
361
+ if self.stopping.is_set():
362
+ self.logger.debug(
363
+ 'Requeuing message received during shutdown: task=%s.%s',
364
+ run.__module__,
365
+ run.__qualname__,
366
+ )
367
+ event.nack()
368
+ return
369
+ if not isinstance(
370
+ event.body,
371
+ Mapping,
372
+ ):
373
+ self.failed(
374
+ run,
375
+ event,
376
+ TypeError('Run message body must be a mapping'),
377
+ )
378
+ return
379
+ self.pending += 1
380
+ consumer = self.consumer
381
+ if consumer is None:
382
+ self.pending -= 1
383
+ raise RuntimeError('Worker consumer is not initialized')
384
+ inspector = self.inspector
385
+ task = run.name
386
+ self.logger.debug(
387
+ 'Task message received: task=%s pending=%s',
388
+ task,
389
+ self.pending,
390
+ )
391
+ contexts = inspector.begin(task) if inspector is not None else ()
392
+ generation = self.generation
393
+ delivery = object()
394
+ with self.delivery_lock:
395
+ self.deliveries[delivery] = lambda: inspector.end(
396
+ task,
397
+ contexts,
398
+ None,
399
+ ) if inspector is not None else None
400
+
401
+ def dispatch(
402
+ error: Optional[BaseException],
403
+ result: Any,
404
+ ) -> None:
405
+ with self.delivery_lock:
406
+ if generation != self.generation or self.finished.is_set():
407
+ self.logger.warning('Discarding stale task result: task=%s', task)
408
+ return
409
+ self.complete(
410
+ run,
411
+ event,
412
+ error,
413
+ result,
414
+ )
415
+ return
416
+
417
+ def schedule(
418
+ error: Optional[BaseException] = None,
419
+ result: Any = None,
420
+ ) -> None:
421
+ with self.delivery_lock:
422
+ if generation != self.generation:
423
+ self.logger.warning('Discarding stale task result: task=%s', task)
424
+ return
425
+ self.deliveries.pop(delivery, None)
426
+ if inspector is not None:
427
+ inspector.end(
428
+ task,
429
+ contexts,
430
+ error is None,
431
+ )
432
+ if self.finished.is_set():
433
+ self.logger.error('Cannot confirm delivery after Consumer exit: task=%s', task)
434
+ return
435
+ consumer.call(lambda: dispatch(error, result))
436
+ return
437
+
438
+ try:
439
+ self.logger.debug(
440
+ 'Submitting task to Pool: task=%s',
441
+ task,
442
+ )
443
+ self.pool.submit(
444
+ run,
445
+ dict(event.body),
446
+ callback=lambda result: schedule(result=result),
447
+ error_callback=schedule,
448
+ )
449
+ self.logger.debug(
450
+ 'Pool task submission accepted: task=%s',
451
+ task,
452
+ )
453
+ except PoolFull:
454
+ self.deliveries.pop(delivery, None)
455
+ self.logger.debug(
456
+ 'Requeuing message because Pool is full: task=%s',
457
+ task,
458
+ )
459
+ if inspector is not None:
460
+ inspector.end(
461
+ task,
462
+ contexts,
463
+ None,
464
+ )
465
+ self.pending -= 1
466
+ event.nack()
467
+ return
468
+ except Exception:
469
+ self.deliveries.pop(delivery, None)
470
+ if inspector is not None:
471
+ inspector.end(
472
+ task,
473
+ contexts,
474
+ None,
475
+ )
476
+ self.pending -= 1
477
+ raise
478
+
479
+ def complete(
480
+ self,
481
+ run: Type[Run],
482
+ event: Event,
483
+ error: Optional[BaseException] = None,
484
+ result: Any = None,
485
+ ) -> None:
486
+ try:
487
+ if error is not None:
488
+ self.failed(
489
+ run,
490
+ event,
491
+ error,
492
+ )
493
+ else:
494
+ if self.consumer is None:
495
+ raise RuntimeError('Worker consumer is not initialized')
496
+ self.consumer.complete(
497
+ event,
498
+ result,
499
+ )
500
+ self.logger.debug(
501
+ 'Consumer completion returned: task=%s.%s',
502
+ run.__module__,
503
+ run.__qualname__,
504
+ )
505
+ finally:
506
+ self.pending -= 1
507
+ self.logger.debug(
508
+ 'Task completion handling finished: pending=%s',
509
+ self.pending,
510
+ )
511
+ self.try_stop()
512
+
513
+ def failed(
514
+ self,
515
+ run: Type[Run],
516
+ event: Event,
517
+ error: BaseException,
518
+ ) -> None:
519
+ if isinstance(
520
+ error,
521
+ TypeError,
522
+ ) and not isinstance(
523
+ event.body,
524
+ Mapping,
525
+ ):
526
+ self.logger.warning(
527
+ 'Rejected a non-mapping task message: task=%s.%s',
528
+ run.__module__,
529
+ run.__qualname__,
530
+ )
531
+ else:
532
+ self.logger.error(
533
+ 'Task failed: task=%s.%s',
534
+ run.__module__,
535
+ run.__qualname__,
536
+ exc_info=(
537
+ type(error),
538
+ error,
539
+ error.__traceback__,
540
+ ),
541
+ )
542
+ if self.consumer is None:
543
+ raise RuntimeError('Worker consumer is not initialized')
544
+ self.consumer.fail(
545
+ event,
546
+ run.on_error,
547
+ )
548
+ self.logger.debug(
549
+ 'Consumer failure policy returned: task=%s.%s',
550
+ run.__module__,
551
+ run.__qualname__,
552
+ )
553
+
554
+
555
+ class Handler(EventHandler):
556
+ def __init__(
557
+ self,
558
+ worker: Worker,
559
+ run: Type[Run],
560
+ ):
561
+ self.worker = worker
562
+ self.run = run
563
+
564
+ def __call__(
565
+ self,
566
+ event: Event,
567
+ ) -> None:
568
+ self.worker.receive(
569
+ self.run,
570
+ event,
571
+ )
@@ -0,0 +1,47 @@
1
+ from .Worker import (
2
+ Worker,
3
+ WorkerSetup,
4
+ WorkerCleanup,
5
+ )
6
+ from .Message import (
7
+ declare,
8
+ emit,
9
+ request,
10
+ )
11
+ from .Run import (
12
+ Run,
13
+ Context,
14
+ RunSetup,
15
+ RunComplete,
16
+ RunError,
17
+ )
18
+ from .Specification import Specification
19
+ from .Pool import (
20
+ Pool,
21
+ PoolFull,
22
+ PoolSetup,
23
+ PoolCleanup,
24
+ ThreadPool,
25
+ ProcessPool,
26
+ )
27
+
28
+ __all__ = (
29
+ 'Worker',
30
+ 'declare',
31
+ 'emit',
32
+ 'request',
33
+ 'WorkerSetup',
34
+ 'WorkerCleanup',
35
+ 'Run',
36
+ 'Specification',
37
+ 'Pool',
38
+ 'PoolFull',
39
+ 'PoolSetup',
40
+ 'PoolCleanup',
41
+ 'ThreadPool',
42
+ 'ProcessPool',
43
+ 'Context',
44
+ 'RunSetup',
45
+ 'RunComplete',
46
+ 'RunError',
47
+ )
DefStream/__init__.py ADDED
File without changes
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: DefStream
3
+ Version: 0.1.22
4
+ Summary: Synchronous message processing with pluggable broker adapters
5
+ Home-page: https://github.com/yong5eon/DefStream
6
+ Author: Heo Yongseon
7
+ Project-URL: Source, https://github.com/yong5eon/DefStream
8
+ Project-URL: Issues, https://github.com/yong5eon/DefStream/issues
9
+ Project-URL: Releases, https://github.com/yong5eon/DefStream/releases
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: rabbitmq
20
+ Requires-Dist: pika>=1.3.2; extra == "rabbitmq"
21
+ Provides-Extra: redis
22
+ Requires-Dist: redis>=5.1.1; extra == "redis"
23
+ Dynamic: author
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: license-file
29
+ Dynamic: project-url
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # DefStream
35
+
36
+ Synchronous event/message processing with pluggable broker adapters
37
+
38
+ DefStream processes broker messages with ordinary Python task methods and can run them in thread or process pools. It requires Python 3.9 or later.
39
+
40
+ ## Installation
41
+
42
+ Install the client extra for the broker you use. A broker server must be available separately.
43
+
44
+ ```shell
45
+ python -m pip install "DefStream[RabbitMQ]"
46
+ ```
47
+
48
+ For Redis Streams, install `DefStream[Redis]`. To install both clients, use `DefStream[RabbitMQ,Redis]`. The plain `DefStream` installation includes neither broker client.
49
+
50
+ ## Quick start
51
+
52
+ The following worker listens to a local RabbitMQ server at `127.0.0.1:5672`. The example credentials are for local development only.
53
+
54
+ ```python
55
+ from signal import SIGINT, signal
56
+
57
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection, Reject, Subscription
58
+ from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
59
+
60
+
61
+ class Add(
62
+ Run,
63
+ parameters=Subscription(queue='example.add', error='example.add.error'),
64
+ on_error=Reject(),
65
+ ):
66
+ def run(self, a: int, b: int):
67
+ print(a + b)
68
+ return a + b
69
+
70
+
71
+ class Prepare(WorkerSetup):
72
+ def __call__(self, worker: Worker) -> None:
73
+ for task in worker.tasks:
74
+ declare(task=task, broker=worker.broker)
75
+
76
+
77
+ if __name__ == '__main__':
78
+ worker = Worker(
79
+ Connection(Configuration(host='127.0.0.1', port=5672, username='guest', password='guest')),
80
+ ThreadPool(4),
81
+ setups=Prepare(),
82
+ )
83
+ worker.add(Add)
84
+ signal(SIGINT, lambda *_: worker.stop())
85
+ worker.run()
86
+ ```
87
+
88
+ Run the script after starting RabbitMQ. The worker declares its task queue and waits for messages. Store production credentials outside source code.
89
+
90
+ ## License
91
+
92
+ See the `LICENSE` file included with the source distribution for the license terms.