DefStream 0.1.16__tar.gz → 0.1.17__tar.gz

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 (179) hide show
  1. defstream-0.1.17/DefStream.egg-info/PKG-INFO +1174 -0
  2. {defstream-0.1.16 → defstream-0.1.17}/DefStream.egg-info/SOURCES.txt +5 -0
  3. defstream-0.1.17/PKG-INFO +1174 -0
  4. {defstream-0.1.16 → defstream-0.1.17}/README.en.md +3 -1
  5. {defstream-0.1.16 → defstream-0.1.17}/README.md +3 -1
  6. defstream-0.1.17/README.setup.md +1141 -0
  7. defstream-0.1.17/issues/fix/26/IMPLEMENTS.md +34 -0
  8. defstream-0.1.17/issues/fix/26/ISSUE.md +13 -0
  9. defstream-0.1.17/issues/fix/26/README.md +5 -0
  10. defstream-0.1.17/issues/fix/26/REQUIREMENTS.md +59 -0
  11. {defstream-0.1.16 → defstream-0.1.17}/setup.py +1 -1
  12. defstream-0.1.16/DefStream.egg-info/PKG-INFO +0 -1152
  13. defstream-0.1.16/PKG-INFO +0 -1152
  14. {defstream-0.1.16 → defstream-0.1.17}/.github/ISSUE_TEMPLATE/01.feature.yml +0 -0
  15. {defstream-0.1.16 → defstream-0.1.17}/.github/ISSUE_TEMPLATE/02.bug.yml +0 -0
  16. {defstream-0.1.16 → defstream-0.1.17}/.github/ISSUE_TEMPLATE/03.fix.yml +0 -0
  17. {defstream-0.1.16 → defstream-0.1.17}/.github/ISSUE_TEMPLATE/04.task.yml +0 -0
  18. {defstream-0.1.16 → defstream-0.1.17}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {defstream-0.1.16 → defstream-0.1.17}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {defstream-0.1.16 → defstream-0.1.17}/.github/workflows/ci.yml +0 -0
  21. {defstream-0.1.16 → defstream-0.1.17}/.gitignore +0 -0
  22. {defstream-0.1.16 → defstream-0.1.17}/DefStream.egg-info/dependency_links.txt +0 -0
  23. {defstream-0.1.16 → defstream-0.1.17}/DefStream.egg-info/requires.txt +0 -0
  24. {defstream-0.1.16 → defstream-0.1.17}/DefStream.egg-info/top_level.txt +0 -0
  25. {defstream-0.1.16 → defstream-0.1.17}/LICENSE +0 -0
  26. {defstream-0.1.16 → defstream-0.1.17}/issues/bug/16/IMPLEMENTS.md +0 -0
  27. {defstream-0.1.16 → defstream-0.1.17}/issues/bug/16/ISSUE.md +0 -0
  28. {defstream-0.1.16 → defstream-0.1.17}/issues/bug/16/README.md +0 -0
  29. {defstream-0.1.16 → defstream-0.1.17}/issues/bug/16/REQUIREMENTS.md +0 -0
  30. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/1/IMPLEMENTS.md +0 -0
  31. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/1/README.md +0 -0
  32. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/1/REQUIREMENTS.md +0 -0
  33. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/1/TASK.md +0 -0
  34. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/1/ci.refs.json +0 -0
  35. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/8/IMPLEMENTS.md +0 -0
  36. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/8/README.md +0 -0
  37. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/8/REQUIREMENTS.md +0 -0
  38. {defstream-0.1.16 → defstream-0.1.17}/issues/feature/8/ci.refs.json +0 -0
  39. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/10/IMPLEMENTS.md +0 -0
  40. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/10/README.md +0 -0
  41. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/10/REQUIREMENTS.md +0 -0
  42. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/13/README.md +0 -0
  43. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/21/IMPLEMENTS.md +0 -0
  44. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/21/ISSUE.md +0 -0
  45. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/21/README.md +0 -0
  46. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/21/REQUIREMENTS.md +0 -0
  47. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/24/IMPLEMENTS.md +0 -0
  48. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/24/ISSUE.md +0 -0
  49. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/24/README.md +0 -0
  50. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/24/REQUIREMENTS.md +0 -0
  51. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/25/IMPLEMENTS.md +0 -0
  52. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/25/ISSUE.md +0 -0
  53. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/25/README.md +0 -0
  54. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/25/REQUIREMENTS.md +0 -0
  55. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/25/TASK.md +0 -0
  56. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/3/IMPLEMENTS.md +0 -0
  57. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/3/README.md +0 -0
  58. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/3/REQUIREMENTS.md +0 -0
  59. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/3/ci.refs.json +0 -0
  60. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/6/IMPLEMENTS.md +0 -0
  61. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/6/README.md +0 -0
  62. {defstream-0.1.16 → defstream-0.1.17}/issues/fix/6/REQUIREMENTS.md +0 -0
  63. {defstream-0.1.16 → defstream-0.1.17}/issues/task/18/IMPLEMENTS.md +0 -0
  64. {defstream-0.1.16 → defstream-0.1.17}/issues/task/18/README.md +0 -0
  65. {defstream-0.1.16 → defstream-0.1.17}/issues/task/18/REQUIREMENTS.md +0 -0
  66. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/RabbitMQ/Sample.RPC.py +0 -0
  67. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/RabbitMQ/Sample.Stream.py +0 -0
  68. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/RabbitMQ/Sample.py +0 -0
  69. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/Redis/Sample.Stream.py +0 -0
  70. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/Redis/Sample.py +0 -0
  71. {defstream-0.1.16 → defstream-0.1.17}/samples/Broker/Sample.py +0 -0
  72. {defstream-0.1.16 → defstream-0.1.17}/samples/Worker/RabbitMQ/Client.py +0 -0
  73. {defstream-0.1.16 → defstream-0.1.17}/samples/Worker/RabbitMQ/Tasks.py +0 -0
  74. {defstream-0.1.16 → defstream-0.1.17}/samples/Worker/RabbitMQ/Worker.py +0 -0
  75. {defstream-0.1.16 → defstream-0.1.17}/samples/Worker/Redis/Sample.py +0 -0
  76. {defstream-0.1.16 → defstream-0.1.17}/samples/Worker/Sample.py +0 -0
  77. {defstream-0.1.16 → defstream-0.1.17}/setup.cfg +0 -0
  78. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Configuration.py +0 -0
  79. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Connection.py +0 -0
  80. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Consumer.py +0 -0
  81. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Errors.py +0 -0
  82. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Event.py +0 -0
  83. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Configuration.py +0 -0
  84. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Connection.py +0 -0
  85. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Consumer.py +0 -0
  86. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Event.py +0 -0
  87. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Exchange.py +0 -0
  88. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Policies.py +0 -0
  89. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Queue.py +0 -0
  90. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Serializer.py +0 -0
  91. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Stream.py +0 -0
  92. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/Subscription.py +0 -0
  93. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/RabbitMQ/__init__.py +0 -0
  94. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Configuration.py +0 -0
  95. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Connection.py +0 -0
  96. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Consumer.py +0 -0
  97. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Event.py +0 -0
  98. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Queue.py +0 -0
  99. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Serializer.py +0 -0
  100. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/Subscription.py +0 -0
  101. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/Redis/__init__.py +0 -0
  102. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Implements/__init__.py +0 -0
  103. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Policies.py +0 -0
  104. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Priority.py +0 -0
  105. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/Protocol.py +0 -0
  106. {defstream-0.1.16 → defstream-0.1.17}/src/Broker/__init__.py +0 -0
  107. {defstream-0.1.16 → defstream-0.1.17}/src/Descriptor/DataObject.py +0 -0
  108. {defstream-0.1.16 → defstream-0.1.17}/src/Descriptor/Types.py +0 -0
  109. {defstream-0.1.16 → defstream-0.1.17}/src/Descriptor/Value.py +0 -0
  110. {defstream-0.1.16 → defstream-0.1.17}/src/Descriptor/__init__.py +0 -0
  111. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extension.py +0 -0
  112. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Console.html +0 -0
  113. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Console.py +0 -0
  114. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Metrics.py +0 -0
  115. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Ping.py +0 -0
  116. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Specifications.py +0 -0
  117. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Status.html +0 -0
  118. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/Status.py +0 -0
  119. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Extensions/__init__.py +0 -0
  120. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Inspector.py +0 -0
  121. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/Response.py +0 -0
  122. {defstream-0.1.16 → defstream-0.1.17}/src/Inspector/__init__.py +0 -0
  123. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Pattern.py +0 -0
  124. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Array.py +0 -0
  125. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Common.py +0 -0
  126. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Compare.py +0 -0
  127. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Condition.py +0 -0
  128. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/DataObject.py +0 -0
  129. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/DateTime.py +0 -0
  130. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Number.py +0 -0
  131. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Object.py +0 -0
  132. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Size.py +0 -0
  133. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/String.py +0 -0
  134. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/Type.py +0 -0
  135. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Patterns/__init__.py +0 -0
  136. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Validate.py +0 -0
  137. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/Validator.py +0 -0
  138. {defstream-0.1.16 → defstream-0.1.17}/src/Validator/__init__.py +0 -0
  139. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Executor.py +0 -0
  140. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Message.py +0 -0
  141. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Pool.py +0 -0
  142. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Run.py +0 -0
  143. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Specification.py +0 -0
  144. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Startup.py +0 -0
  145. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/Worker.py +0 -0
  146. {defstream-0.1.16 → defstream-0.1.17}/src/Worker/__init__.py +0 -0
  147. {defstream-0.1.16 → defstream-0.1.17}/src/__init__.py +0 -0
  148. {defstream-0.1.16 → defstream-0.1.17}/tests/test_broker_packaging.py +0 -0
  149. {defstream-0.1.16 → defstream-0.1.17}/tests/test_descriptor.py +0 -0
  150. {defstream-0.1.16 → defstream-0.1.17}/tests/test_inspector.py +0 -0
  151. {defstream-0.1.16 → defstream-0.1.17}/tests/test_inspector_console.py +0 -0
  152. {defstream-0.1.16 → defstream-0.1.17}/tests/test_inspector_logging.py +0 -0
  153. {defstream-0.1.16 → defstream-0.1.17}/tests/test_lifecycle_hooks.py +0 -0
  154. {defstream-0.1.16 → defstream-0.1.17}/tests/test_logging.py +0 -0
  155. {defstream-0.1.16 → defstream-0.1.17}/tests/test_pool_callbacks.py +0 -0
  156. {defstream-0.1.16 → defstream-0.1.17}/tests/test_process_signals.py +0 -0
  157. {defstream-0.1.16 → defstream-0.1.17}/tests/test_protocol_contracts.py +0 -0
  158. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_broker_methods.py +0 -0
  159. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_integration.py +0 -0
  160. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_logging.py +0 -0
  161. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_priorities.py +0 -0
  162. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_release_workflow.py +0 -0
  163. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_rpc.py +0 -0
  164. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_rpc_integration.py +0 -0
  165. {defstream-0.1.16 → defstream-0.1.17}/tests/test_rabbitmq_worker.py +0 -0
  166. {defstream-0.1.16 → defstream-0.1.17}/tests/test_redis.py +0 -0
  167. {defstream-0.1.16 → defstream-0.1.17}/tests/test_redis_integration.py +0 -0
  168. {defstream-0.1.16 → defstream-0.1.17}/tests/test_redis_logging.py +0 -0
  169. {defstream-0.1.16 → defstream-0.1.17}/tests/test_redis_release_workflow.py +0 -0
  170. {defstream-0.1.16 → defstream-0.1.17}/tests/test_release_workflow.py +0 -0
  171. {defstream-0.1.16 → defstream-0.1.17}/tests/test_runner_hooks.py +0 -0
  172. {defstream-0.1.16 → defstream-0.1.17}/tests/test_specifications.py +0 -0
  173. {defstream-0.1.16 → defstream-0.1.17}/tests/test_status.py +0 -0
  174. {defstream-0.1.16 → defstream-0.1.17}/tests/test_status_ui.cjs +0 -0
  175. {defstream-0.1.16 → defstream-0.1.17}/tests/test_task_messages.py +0 -0
  176. {defstream-0.1.16 → defstream-0.1.17}/tests/test_task_priorities.py +0 -0
  177. {defstream-0.1.16 → defstream-0.1.17}/tests/test_validator_patterns.py +0 -0
  178. {defstream-0.1.16 → defstream-0.1.17}/tests/test_worker.py +0 -0
  179. {defstream-0.1.16 → defstream-0.1.17}/tests/test_worker_startup.py +0 -0
@@ -0,0 +1,1174 @@
1
+ Metadata-Version: 2.4
2
+ Name: DefStream
3
+ Version: 0.1.17
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
+ **Just def.**<br>
39
+ **Let workers do the work.**<br>
40
+ **You don't need to await this.**
41
+
42
+ DefStream is a synchronous task-processing library that consumes broker messages
43
+ and executes ordinary Python task methods in a thread or process pool.
44
+ Configure execution, acknowledgements, failure policies, and monitoring without
45
+ `async` / `await`.
46
+
47
+ - **Task definitions**: declare a subscription and handler in a `Run` class.
48
+ - **Parallel execution**: choose `ThreadPool` or `ProcessPool`.
49
+ - **Broker choice**: RabbitMQ and Redis Streams implementations are included; install only the clients you need.
50
+ - **Request/reply**: receive task return values through RabbitMQ RPC.
51
+ - **Operations**: optionally enable HTTP health checks, Prometheus metrics, a dashboard, and a task console.
52
+ - **Input/output contracts**: define schemas and runtime validators and expose AsyncAPI documentation.
53
+
54
+ ## Contents
55
+
56
+ - [Installation](#installation)
57
+ - [Quick start](#quick-start)
58
+ - [Tasks and message handling](#tasks-and-message-handling)
59
+ - [Execution pools and shutdown](#execution-pools-and-shutdown)
60
+ - [Lifecycle hooks](#lifecycle-hooks)
61
+ - [Request/reply contracts and validation](#requestreply-contracts-and-validation)
62
+ - [Health checks and task console](#health-checks-and-task-console)
63
+ - [Logging](#logging)
64
+ - [Broker selection and operational considerations](#broker-selection-and-operational-considerations)
65
+ - [RabbitMQ usage](#rabbitmq-usage)
66
+ - [Redis usage](#redis-usage)
67
+ - [Examples and license](#examples-and-license)
68
+
69
+ ## Logging
70
+
71
+ DefStream records messages through standard `logging` without configuring handlers,
72
+ formatters, or levels. Select the sources your application needs. The samples
73
+ output only the `Sample` and `DefStream` logger hierarchies, replacing their
74
+ `print()` calls with logging. The Worker's existing startup banner is still
75
+ printed regardless of logging configuration.
76
+
77
+ ```python
78
+ from logging import DEBUG, Filter, Formatter, StreamHandler, getLogger
79
+
80
+ sources = (Filter('Sample'), Filter('DefStream'))
81
+ handler = StreamHandler()
82
+ handler.setFormatter(Formatter(
83
+ '%(asctime)s %(processName)s %(name)s %(levelname)s %(message)s'
84
+ ))
85
+ handler.addFilter(lambda record: any(source.filter(record) for source in sources))
86
+ root = getLogger()
87
+ root.setLevel(DEBUG)
88
+ root.addHandler(handler)
89
+
90
+ logger = getLogger('Sample')
91
+ logger.info('Application started')
92
+ ```
93
+
94
+ Apply filters to the **handler**, not the root logger. `Filter('DefStream')`
95
+ includes `DefStream` and names below `DefStream.`, but not `DefStreamExtra`.
96
+ This configuration is an example for a standalone application; it does not change
97
+ output from other handlers already installed. To receive only broker logs, select
98
+ `DefStream.Broker.Implements.RabbitMQ` or `DefStream.Broker.Implements.Redis`
99
+ instead of `DefStream`.
100
+
101
+ - The class member `logger` uses `DefStream.Worker`, `DefStream.Run`,
102
+ `DefStream.Executor`, `DefStream.Pool`, `DefStream.ThreadPool`, and
103
+ `DefStream.ProcessPool`.
104
+ - Shared Pool methods also use the concrete pool's logger member. User subclasses
105
+ inherit the default name and can explicitly override `logger`.
106
+ - Inspector, Server, and RequestHandler share `DefStream.Inspector`.
107
+ HTTP request receipt is DEBUG; response status and transmission results are INFO.
108
+ Extension-specific logs use `DefStream.Inspector.Extensions.<extension name>`.
109
+ - Normal per-message processing is DEBUG, lifecycle events are INFO, events
110
+ requiring attention are WARNING, and processing failures are ERROR.
111
+ Bodies, results, and credentials are excluded from the default log context.
112
+ - Without any handlers, Python's `lastResort` may write WARNING and higher to
113
+ stderr. Do not put sensitive values in user exception messages: failure
114
+ tracebacks include those messages.
115
+
116
+ ProcessPool uses `spawn` child processes, so parent handlers are not automatically
117
+ inherited. To receive child logs, apply the same configuration in an importable
118
+ `PoolSetup` implementation and pass it through `ProcessPool(..., setups=...)`.
119
+ That configuration cannot capture DEBUG and INFO logs emitted before the child
120
+ setup hook runs. In existing applications, choose the initialization location
121
+ carefully to avoid adding the same handlers repeatedly.
122
+
123
+ ## Installation
124
+
125
+ Requires Python **3.9 or newer**. Prepare the broker server you intend to use.
126
+
127
+ ```shell
128
+ python -m pip install "DefStream[RabbitMQ]"
129
+ ```
130
+
131
+ Choose Redis or both adapters as needed:
132
+
133
+ ```shell
134
+ python -m pip install "DefStream[Redis]"
135
+ python -m pip install "DefStream[RabbitMQ,Redis]"
136
+ ```
137
+
138
+ For the core alone, use `python -m pip install DefStream`.
139
+ Both broker implementations are included in the DefStream distribution, but the default
140
+ installation has no broker client dependencies. The `RabbitMQ` extra installs `pika>=1.3.2`;
141
+ the `Redis` extra installs `redis>=5.1.1`.
142
+
143
+ | Installation choice | Python import path | Guide |
144
+ | --- | --- | --- |
145
+ | `DefStream` | `DefStream` | This document |
146
+ | `DefStream[RabbitMQ]` | `DefStream.Broker.Implements.RabbitMQ` | [RabbitMQ usage](#rabbitmq-usage) |
147
+ | `DefStream[Redis]` | `DefStream.Broker.Implements.Redis` | [Redis usage](#redis-usage) |
148
+
149
+ Separate releases of `DefStream-RabbitMQ` and `DefStream-Redis` are discontinued,
150
+ and their projects are scheduled for deletion. Backward compatibility with those
151
+ packages is not supported. Install the merged package in a new environment using
152
+ the commands above. To migrate an existing environment, first run
153
+ `python -m pip uninstall DefStream-RabbitMQ DefStream-Redis`, then reinstall the
154
+ merged DefStream. Do not install them together: they provide files at the same import paths.
155
+ For an existing environment, use
156
+ `python -m pip install --force-reinstall "DefStream[RabbitMQ,Redis]"`,
157
+ selecting extras for the brokers you actually use.
158
+
159
+ ## Quick start
160
+
161
+ These examples assume a local RabbitMQ server at `127.0.0.1:5672`.
162
+ Save the following three files in the same directory. The `guest` credentials
163
+ are for local examples only; use a dedicated account and environment variables
164
+ or a secret store in production.
165
+
166
+ ### 1. Define a task: `tasks.py`
167
+
168
+ ```python
169
+ from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
170
+ from DefStream.Worker import Run
171
+
172
+
173
+ class Add(
174
+ Run,
175
+ parameters=Subscription(
176
+ queue='defstream.example.add',
177
+ qos=4,
178
+ error='defstream.example.add.error',
179
+ ),
180
+ on_error=Reject(),
181
+ ):
182
+ def run(
183
+ self,
184
+ a: int,
185
+ b: int,
186
+ ):
187
+ result = a + b
188
+ print(result)
189
+ return result
190
+ ```
191
+
192
+ `parameters` selects the task's queue. A body of `{'a': 10, 'b': 20}` becomes
193
+ `run(a=10, b=20)`. This example rejects failures instead of retrying them and
194
+ declares an error queue to receive rejected messages.
195
+
196
+ ### 2. Run the Worker: `worker.py`
197
+
198
+ `Worker` accepts `DefStream.Broker.Connection` directly.
199
+ The connection contract only requires `connect()` and `close()`. The worker checks
200
+ for `GetConsumer` support internally when it runs. A broker that cannot create
201
+ a consumer raises `TypeError`; no worker-specific connection type is needed.
202
+
203
+ ```python
204
+ from signal import SIGINT, signal
205
+
206
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
207
+ from DefStream.Worker import ThreadPool, Worker, WorkerSetup, declare
208
+ from tasks import Add
209
+
210
+
211
+ class Prepare(WorkerSetup):
212
+ def __call__(
213
+ self,
214
+ worker: Worker,
215
+ ) -> None:
216
+ for task in worker.tasks:
217
+ declare(
218
+ task=task,
219
+ broker=worker.broker,
220
+ )
221
+
222
+
223
+ if __name__ == '__main__':
224
+ worker = Worker(
225
+ Connection(Configuration(
226
+ host='127.0.0.1',
227
+ port=5672,
228
+ username='guest',
229
+ password='guest',
230
+ )),
231
+ ThreadPool(4),
232
+ setups=Prepare(),
233
+ )
234
+ worker.add(Add)
235
+ signal(
236
+ SIGINT,
237
+ lambda *_: worker.stop(),
238
+ )
239
+ worker.run()
240
+ ```
241
+
242
+ ```shell
243
+ python worker.py
244
+ ```
245
+
246
+ The Worker opens its broker connection, runs `Prepare` to declare the queue
247
+ and error queue, then starts consuming. Defining a `Run` does not register it:
248
+ call `worker.add()` explicitly.
249
+
250
+ ### 3. Send messages: `client.py`
251
+
252
+ Run this in another terminal while the Worker is running.
253
+
254
+ ```python
255
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
256
+ from DefStream.Worker import emit, request
257
+ from tasks import Add
258
+
259
+
260
+ connection = Connection(Configuration(
261
+ host='127.0.0.1',
262
+ port=5672,
263
+ username='guest',
264
+ password='guest',
265
+ ))
266
+ try:
267
+ connection.connect()
268
+ message_id = emit(
269
+ {'a': 10, 'b': 20},
270
+ task=Add,
271
+ broker=connection,
272
+ )
273
+ print(message_id)
274
+ result = request(
275
+ {'a': 10, 'b': 20},
276
+ task=Add,
277
+ broker=connection,
278
+ timeout=5,
279
+ )
280
+ print(result)
281
+ finally:
282
+ connection.close()
283
+ ```
284
+
285
+ ```shell
286
+ python client.py
287
+ ```
288
+
289
+ `emit()` and `request()` **each publish a separate task**. This example performs
290
+ the addition twice; `request()` returns `30`. The request timeout is in seconds
291
+ and does not cancel work that has already been delivered.
292
+
293
+ ## Tasks and message handling
294
+
295
+ ### Task-directed API
296
+
297
+ | Function | Purpose | Connection ownership |
298
+ | --- | --- | --- |
299
+ | `declare(task=..., broker=...)` | Declare task queues and subscription resources | Caller connects and closes |
300
+ | `emit(body, task=..., broker=...)` | Publish a one-way message to a task | Caller connects and closes |
301
+ | `request(body, task=..., broker=..., timeout=5)` | Request a task result synchronously | Caller connects and closes; RabbitMQ only |
302
+
303
+ Import these functions from `DefStream.Worker`. They do not connect or close
304
+ the supplied connection. Neither `emit()` nor `request()` declares destinations.
305
+ Call `declare()` in the receiving Worker's setup or a separate provisioning step.
306
+ Reuse a connection for multiple publications on its owning thread.
307
+
308
+ All three helpers accept the common `Connection` type and check declaration,
309
+ emission, or RPC support internally. Unsupported operations raise `NotImplementedError`.
310
+ The task helper and connection method previously named `send` are now named `emit`.
311
+ Custom brokers should use `Emittable.emit` instead of `TaskSender.send` for task emission.
312
+ Low-level `Sendable.send()` and RPC `Callable.call()` on queues, exchanges, and streams
313
+ remain unchanged.
314
+
315
+ Task bodies must be **mappings with string keys**. Return values must be
316
+ serializable by the adapter. A Worker cannot register multiple tasks for the
317
+ same queue: routing selects tasks by queue, not by a type field in the message.
318
+
319
+ ### Task priorities
320
+
321
+ For RabbitMQ classic queues, define both the default message `priority` and queue
322
+ `max_priority` in the RabbitMQ `Subscription` supplied as the task's `parameters`.
323
+ There is no separate `priority` setting on `Run`.
324
+ `declare()` reads these settings to provision the priority queue.
325
+
326
+ ```python
327
+ from DefStream.Broker.Implements.RabbitMQ import Subscription
328
+ from DefStream.Worker import (
329
+ Run,
330
+ declare,
331
+ request,
332
+ emit,
333
+ )
334
+
335
+
336
+ class Add(
337
+ Run,
338
+ parameters=Subscription(
339
+ queue='defstream.example.priority.add',
340
+ max_priority=5,
341
+ priority=2,
342
+ ),
343
+ ):
344
+ def run(
345
+ self,
346
+ a: int,
347
+ b: int,
348
+ ):
349
+ return a + b
350
+
351
+
352
+ # broker is a RabbitMQ Connection connected by the caller.
353
+ declare(
354
+ task=Add,
355
+ broker=broker,
356
+ )
357
+ emit(
358
+ {'a': 10, 'b': 20},
359
+ task=Add,
360
+ broker=broker,
361
+ )
362
+ # RPC requires a separately running Worker.
363
+ result = request(
364
+ {'a': 10, 'b': 20},
365
+ task=Add,
366
+ broker=broker,
367
+ priority=5,
368
+ )
369
+ ```
370
+
371
+ - Omitting the call's `priority` or passing `None` uses the task default.
372
+ An explicit `0` overrides the default with the lowest priority.
373
+ - Setting only the task default sets the queue maximum to RabbitMQ's limit of
374
+ `255`. Setting only the maximum leaves the default as `None`; without a
375
+ per-call value either, the message priority property is omitted.
376
+ - Values above the task maximum, negative values, non-integers, and `bool` are rejected.
377
+ - Specifying priority only at call time validates only the broker range `0..255`.
378
+ The caller must provision a priority queue and respect its actual maximum.
379
+ - Child tasks inherit settings and may override them with valid values, but cannot
380
+ clear them.
381
+ - Requesting priorities with Redis raises `NotImplementedError`.
382
+ Calls without priorities retain existing Redis behavior.
383
+ - Priority affects delivery of waiting messages within the same queue. It does not
384
+ preempt delivered or running tasks or guarantee execution and completion order.
385
+
386
+ ### Success, failures, and retries
387
+
388
+ - ACK follows successful execution and completion hooks. RabbitMQ RPC also publishes the reply before ACK.
389
+ - Failures use the task's `on_error` policy. The default is `Requeue()`; choose `Reject()` explicitly if needed.
390
+ - Both policies can be imported from `DefStream.Broker.Policies`.
391
+ - Rejection is broker-specific: RabbitMQ dead-letters to a configured DLQ or discards the message;
392
+ Redis acknowledges it for the group while preserving stream history.
393
+ - There is no automatic retry-count limit. Choose a policy that avoids endless redelivery of permanently invalid messages.
394
+ - Publication and ACK are not one transaction. **Make tasks idempotent and safe to execute more than once.**
395
+
396
+ The common resource operations below apply when accessing a broker directly.
397
+ Not every resource supports every operation.
398
+
399
+ Use `isinstance` or `issubclass` with `GetExchange`, `GetQueue`, `GetStream`,
400
+ or `GetConsumer` to check whether a connection provides `exchange()`, `queue()`,
401
+ `stream()`, or `consumer()`, respectively. Import these contracts from `DefStream.Broker`.
402
+
403
+ | Method | Purpose | Timeout unit |
404
+ | --- | --- | --- |
405
+ | `send(body)` | Publish a message | Adapter configuration |
406
+ | `call(body, timeout=5)` | Synchronous RPC | Seconds |
407
+ | `pop(timeout=1000)` | Receive one message | Milliseconds |
408
+ | `read(offset=0, timeout=1000)` | Read stream history and new messages | Milliseconds |
409
+
410
+ For `pop()` and `read()`, `None` or `0` waits indefinitely.
411
+ Settle directly consumed processing events with `ack()`, `nack()`, or `reject()`.
412
+ Redis history reads are separate from consumer-group processing and cannot be ACKed.
413
+ RabbitMQ stream reads are ACKed automatically when iteration advances.
414
+
415
+ ## Execution pools and shutdown
416
+
417
+ | Pool | Typical use | Considerations |
418
+ | --- | --- | --- |
419
+ | `ThreadPool(4)` | I/O-bound tasks | Synchronize state shared across execution threads |
420
+ | `ProcessPool(4)` | CPU-bound tasks | Classes, inputs, results, and hooks must be picklable |
421
+
422
+ Import both pools from `DefStream.Worker`. Without an explicit size, they use
423
+ the CPU count. Capacity includes submitted, unfinished tasks; there is no
424
+ unbounded waiting queue. A full pool causes the Worker to NACK for redelivery.
425
+ RabbitMQ's per-task `Subscription.qos` limits unacknowledged deliveries, not pool size.
426
+
427
+ With `ProcessPool`, define classes and hooks at importable module scope and
428
+ create and run the Worker inside `if __name__ == '__main__':`.
429
+ Never pass broker connections or events to child processes.
430
+
431
+ `worker.stop()` stops receiving new work, waits for active tasks and message
432
+ settlement, then closes resources. There is currently no forced termination or
433
+ shutdown timeout. Register an application `SIGINT` handler as above for graceful
434
+ Ctrl+C shutdown. Each Worker, its connection, and its pool belong to a single
435
+ run: do not share them with another Worker or restart them.
436
+
437
+ ## Lifecycle hooks
438
+
439
+ All hook interfaces are exported from `DefStream.Worker`.
440
+
441
+ | Scope | Hooks | Arguments and execution location |
442
+ | --- | --- | --- |
443
+ | Task | `RunSetup`, `RunComplete`, `RunError` | `Context`; context and result; context and exception. Inside the pool |
444
+ | Pool | `PoolSetup`, `PoolCleanup` | No arguments. Inside each execution thread/process |
445
+ | Worker | `WorkerSetup`, `WorkerCleanup` | Worker instance. Parent connection-owning thread |
446
+
447
+ Declare task hooks with `setups=`, `completes=`, and `errors=` on the `Run` class.
448
+ Pass `setups=` and `cleanups=` to Worker or pool constructors.
449
+ Each option accepts one hook or an ordered sequence of hooks.
450
+ Inspect call arguments through `Context.args` and `Context.kwargs`.
451
+
452
+ Pool execution environments start lazily and run their setup and cleanup once
453
+ per environment. Create and close environment-specific resources, such as DB
454
+ connections, there. Worker setup runs after broker connection and before
455
+ subscription; cleanup runs after pool shutdown and before consumer/broker closure.
456
+ Cleanup must tolerate partial initialization. Hooks are not guaranteed after
457
+ forced termination or process crashes.
458
+
459
+ `RunError` is execution-error code; `on_error` is the message-settlement policy.
460
+ Running an error hook does not turn failure into success. If a completion hook
461
+ publishes another task, create and close a separate connection in that hook's
462
+ thread/process. A later failure does not roll back messages already published.
463
+
464
+ ## Request/reply contracts and validation
465
+
466
+ `Descriptor` **describes data structure**; `Validator` **validates and transforms
467
+ actual values**. Connect both to `Specification` to document and validate tasks.
468
+ Place the following examples in the same file, in order.
469
+
470
+ ### 1. Describe a schema with Descriptor
471
+
472
+ ```python
473
+ from DefStream.Descriptor import DumpSchema, Integer, Object, Properties, Schema
474
+
475
+
476
+ request_schema = Object(
477
+ description='Addition request',
478
+ properties=Properties(
479
+ a=Integer('First operand'),
480
+ b=Integer('Second operand'),
481
+ ),
482
+ )
483
+ response_schema = Integer('Sum')
484
+
485
+ print(DumpSchema(Schema(
486
+ 'AddRequest',
487
+ request_schema,
488
+ )))
489
+ ```
490
+
491
+ `Object` describes an object, `Properties` its fields, and `Integer` an integer.
492
+ Fields such as `Integer` are required by default; use `required=False` to mark
493
+ an optional field. Use `String`, `Number`, `Boolean`, and `Array` for other types
494
+ and nested structures. Wrap a value in `Schema` before passing it to
495
+ `DumpSchema()` to obtain a plain JSON Schema dictionary.
496
+
497
+ This documents `a` and `b` as required integers but **does not validate messages
498
+ automatically**. Required fields, defaults, and ranges need corresponding
499
+ validation rules if they must also be enforced at runtime.
500
+
501
+ ### 2. Validate values with Validator
502
+
503
+ ```python
504
+ from DefStream.Validator import Validator
505
+ from DefStream.Validator.Patterns import IsInteger, IsObject
506
+ from DefStream.Validator.Patterns.Object import IsMappingOf, IsRequiredIn
507
+
508
+
509
+ request_validator = Validator(
510
+ IsObject(
511
+ IsRequiredIn(
512
+ 'a',
513
+ 'b',
514
+ ),
515
+ IsMappingOf({
516
+ 'a': IsInteger(),
517
+ 'b': IsInteger(),
518
+ }),
519
+ ),
520
+ )
521
+ response_validator = Validator(IsInteger())
522
+
523
+ print(request_validator({'a': 10, 'b': 20}))
524
+ ```
525
+
526
+ - `Validator` applies patterns in order and returns the final value.
527
+ - `IsObject` checks for the `dict` type, then runs its nested required-key and field-type rules.
528
+ - `IsRequiredIn` raises `ValueError` for missing keys; `IsInteger` raises `TypeError`
529
+ for wrong types, including the string `'10'` and boolean `True`.
530
+ - `IsMappingOf` validates/transforms configured fields and may update the dictionary
531
+ in place. Pass a copy to preserve the original. It does not remove or reject
532
+ unconfigured extra keys. Extra keys that do not match task arguments make the
533
+ call fail; implement additional restrictions if needed.
534
+
535
+ ### 3. Combine conversion and validation
536
+
537
+ To accept numeric strings, use an explicit conversion pattern instead of
538
+ `IsInteger`. This independent example converts a string to an integer and then
539
+ checks that it is non-negative.
540
+
541
+ ```python
542
+ from DefStream.Validator import Validator
543
+ from DefStream.Validator.Patterns import IsGreaterEqualTo, ToInteger
544
+
545
+
546
+ non_negative_integer = Validator(
547
+ ToInteger(),
548
+ IsGreaterEqualTo(0),
549
+ )
550
+
551
+ print(non_negative_integer('10'))
552
+ ```
553
+
554
+ The result is integer `10`, not a string. Invalid strings and negative values
555
+ raise `ValueError`. `ToInteger` handles Python numeric-literal strings and
556
+ differs from strict type checking. This converter does not change the strict
557
+ request validator above. To apply it to a field, set that field's value in
558
+ `IsMappingOf` to `non_negative_integer`.
559
+
560
+ ### 4. Connect task request and response contracts
561
+
562
+ Replace the earlier `Add` with the following class using the schemas and
563
+ validators defined above. Once registered with the Worker, inputs are validated
564
+ before `run()`, and the return value before completion hooks.
565
+
566
+ ```python
567
+ from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
568
+ from DefStream.Worker import Run, Specification
569
+
570
+
571
+ class Add(
572
+ Run,
573
+ parameters=Subscription(queue='defstream.example.validated-add'),
574
+ on_error=Reject(),
575
+ request=Specification(
576
+ schema=request_schema,
577
+ validator=request_validator,
578
+ example={'a': 10, 'b': 20},
579
+ ),
580
+ response=Specification(
581
+ schema=response_schema,
582
+ validator=response_validator,
583
+ example=30,
584
+ ),
585
+ name='add',
586
+ description='Add two integers',
587
+ ):
588
+ def run(
589
+ self,
590
+ a: int,
591
+ b: int,
592
+ ):
593
+ return a + b
594
+ ```
595
+
596
+ - `schema` is documentation metadata: it accepts JSON Schema mappings, Descriptor values/conditions,
597
+ and dataclass types, but does not itself validate values or instantiate models.
598
+ - `validator` is a callable validation/transformation function. Its returned value is used for execution.
599
+ A request validator must still return a mapping with string keys.
600
+ - Plain JSON Schema dictionaries and custom validation functions can also be passed as `schema` and `validator`.
601
+ - `example` is documentation data and is not automatically executed or validated.
602
+ - `format` defaults to `application/json`. It changes documented media types, not broker codecs.
603
+ - Run's `name` is used as both the AsyncAPI Operation key and the Metrics task key.
604
+ When omitted, `module.__qualname__` is used; Run's `description` appears on the AsyncAPI Operation.
605
+ Explicit names must match `[A-Za-z0-9_.-]+`; whitespace and Korean characters are not allowed.
606
+ Names are case-sensitive and preserved exactly as provided.
607
+ - Execution order: setup hooks → request validation → `run()` → response validation → completion hooks → reply/ACK.
608
+
609
+ Validation exceptions are task failures and follow error hooks and the `on_error`
610
+ policy. The example's `Reject()` avoids endlessly retrying invalid inputs.
611
+ To preserve failures in a DLQ, also configure `Subscription(error=...)` and
612
+ provision it through `declare()`. Schemas and examples appear in Inspector's
613
+ AsyncAPI and console; custom Validator patterns are not automatically
614
+ reverse-engineered into JSON Schema.
615
+
616
+ ## Health checks and task console
617
+
618
+ Inspector is optional and uses a Python standard-library HTTP server.
619
+ Configure it before starting the Worker and pass `Worker(..., inspector=inspector)`.
620
+ This snippet reuses the `Connection` and `Configuration` imports from `worker.py`.
621
+
622
+ ```python
623
+ from DefStream.Inspector import Inspector
624
+ from DefStream.Inspector.Extensions import (
625
+ Console,
626
+ Metrics,
627
+ Ping,
628
+ Specifications,
629
+ Status,
630
+ )
631
+
632
+
633
+ def create_connection():
634
+ return Connection(Configuration(
635
+ host='127.0.0.1',
636
+ port=5672,
637
+ username='guest',
638
+ password='guest',
639
+ ))
640
+
641
+
642
+ inspector = Inspector(
643
+ host='127.0.0.1',
644
+ port=9999,
645
+ exts=(
646
+ Ping('/ping'),
647
+ Metrics('/metrics'),
648
+ Status('/status'),
649
+ Specifications('/asyncapi.json'),
650
+ Console(
651
+ '/console',
652
+ broker=create_connection,
653
+ title='DefStream',
654
+ ),
655
+ ),
656
+ )
657
+ ```
658
+
659
+ | Path | Purpose |
660
+ | --- | --- |
661
+ | `/ping` | Check subscription readiness and consumer-loop responsiveness: 200 when healthy, 503 during startup/shutdown or when unresponsive |
662
+ | `/metrics` | Prometheus metrics with the `defstream_` prefix |
663
+ | `/status` | Successes, failures, throughput, and duration; register with exactly one `Metrics` |
664
+ | `/asyncapi.json` | Registered task request/reply contracts as AsyncAPI JSON |
665
+ | `/console` | Browse tasks and schemas, send JSON messages, and execute RPC on supported brokers |
666
+
667
+ There are no default routes. Metrics describe one Worker and reset on restart.
668
+ Execution counts include retries and hooks, not unique messages or ACKs.
669
+ The dashboard provides recent 1-second and 60-second windows, not long-term storage.
670
+ Declaring a response contract does not add RPC support to Redis.
671
+
672
+ **Restrict access.** The console example above publishes real tasks without
673
+ authentication. For authentication, pass a random token of at least 16 characters
674
+ using `Console(token=...)` and enter it in the browser. Load the token from an
675
+ environment variable or secret store. Other built-in endpoints remain
676
+ unauthenticated: bind to loopback or a trusted management network, and use an
677
+ HTTPS reverse proxy with additional access control for remote access.
678
+ Inspector has no built-in TLS and is not a public web server.
679
+ The console's connection factory must return a new connection on every call,
680
+ never the Worker's connection.
681
+
682
+ Console's `broker` parameter uses the `DefStream.Broker.BrokerFactory` contract.
683
+ Its `__call__() -> Connection` takes no arguments and returns a new connection.
684
+ Functions such as `create_connection` above and lambdas remain valid without changes.
685
+ For a class-based factory, you can inherit `BrokerFactory` and implement `__call__`.
686
+
687
+ ## RabbitMQ usage
688
+
689
+ Requires Python 3.9 or newer and a reachable RabbitMQ server; uses Pika.
690
+
691
+ ### Send and receive queue messages
692
+
693
+ This example uses a local RabbitMQ server's `guest` account. Use a dedicated
694
+ account and environment variables or a secret store in production.
695
+ It creates `defstream.example.rabbitmq` and leaves the queue in place.
696
+
697
+ ```python
698
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
699
+
700
+
701
+ connection = Connection(Configuration(
702
+ host='127.0.0.1',
703
+ port=5672,
704
+ username='guest',
705
+ password='guest',
706
+ ))
707
+ try:
708
+ connection.connect()
709
+ connection.create_queue('defstream.example.rabbitmq')
710
+ queue = connection.queue('defstream.example.rabbitmq')
711
+ queue.send({'message': 'Hello, DefStream!'})
712
+ event = queue.pop(timeout=1000)
713
+ if event is not None:
714
+ print(event.body)
715
+ event.ack()
716
+ finally:
717
+ connection.close()
718
+ ```
719
+
720
+ The default codec serializes messages as JSON.
721
+ The `pop()` timeout is in **milliseconds**; a positive timeout returns `None`
722
+ if no delivery arrives. `None` or `0` waits indefinitely.
723
+ Call `ack()` on success, `nack()` for redelivery, or `reject()` to reject.
724
+ Rejected messages go to the broker-configured dead-letter destination or are
725
+ discarded when none is configured.
726
+
727
+ ### Logging
728
+
729
+ `Connection`, `Consumer`, `Queue`, `Exchange`, `Stream`, and `Event` expose a class
730
+ member `logger` named `DefStream.Broker.Implements.RabbitMQ.ClassName`. Subclasses
731
+ inherit that logger unless they explicitly override it. The library adds no handlers
732
+ (including `NullHandler`), levels, or filters; propagation and `logging.lastResort`
733
+ keep their standard behavior.
734
+
735
+ Lifecycle and resource changes use INFO, per-message/RPC/settlement tracing uses DEBUG,
736
+ expected RPC anomalies use WARNING, and internal I/O or codec failures use ERROR.
737
+ Payloads, results, headers, credentials, exception text, and tracebacks are omitted.
738
+ Exceptions still propagate; application-side logging may duplicate a library record.
739
+ A publish return does not mean consumer processing completed, and an RPC response
740
+ timeout does not cancel delivered work.
741
+
742
+ The Python samples configure a root DEBUG console handler only in their main entry
743
+ points. Its filter accepts `Filter('Sample')` **or**
744
+ `Filter('DefStream.Broker.Implements.RabbitMQ')`, including dotted descendants only;
745
+ Pika and core logs are excluded. Applications configure their own handlers, levels,
746
+ and filters at a chosen ancestor (`DefStream`, `DefStream.Broker`, etc.) or class.
747
+
748
+ ### Connection configuration
749
+
750
+ | Option | Meaning |
751
+ | --- | --- |
752
+ | `host`, `port` | RabbitMQ address and AMQP port |
753
+ | `username`, `password` | Credentials |
754
+ | `vhost` | Virtual host; defaults to `/` |
755
+ | `ssl` | Enable TLS; defaults to `False` |
756
+ | `timeout` | Blocked-connection timeout, in milliseconds |
757
+ | `heartbeat` | The current implementation passes `int(heartbeat * 2 / 1000)` seconds to Pika |
758
+ | `encode`, `decode` | Message codecs; JSON by default |
759
+
760
+ `timeout` is not a whole-operation deadline.
761
+ Configure and service connection heartbeats appropriately for task and idle periods.
762
+ Use connections and their resources **only on the owning thread**; do not
763
+ share them across threads or processes.
764
+
765
+ ### Process tasks with a Worker
766
+
767
+ This executable example provisions a task queue and error queue, then consumes tasks.
768
+
769
+ ```python
770
+ from signal import SIGINT, signal
771
+
772
+ from DefStream.Broker.Implements.RabbitMQ import (
773
+ Configuration,
774
+ Connection,
775
+ Reject,
776
+ Subscription,
777
+ )
778
+ from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
779
+
780
+
781
+ class Add(
782
+ Run,
783
+ parameters=Subscription(
784
+ queue='defstream.example.rabbitmq.add',
785
+ qos=4,
786
+ error='defstream.example.rabbitmq.add.error',
787
+ ),
788
+ on_error=Reject(),
789
+ ):
790
+ def run(
791
+ self,
792
+ a: int,
793
+ b: int,
794
+ ):
795
+ return a + b
796
+
797
+
798
+ class Prepare(WorkerSetup):
799
+ def __call__(
800
+ self,
801
+ worker: Worker,
802
+ ) -> None:
803
+ for task in worker.tasks:
804
+ declare(
805
+ task=task,
806
+ broker=worker.broker,
807
+ )
808
+
809
+
810
+ if __name__ == '__main__':
811
+ worker = Worker(
812
+ Connection(Configuration(
813
+ host='127.0.0.1',
814
+ port=5672,
815
+ username='guest',
816
+ password='guest',
817
+ )),
818
+ ThreadPool(4),
819
+ setups=Prepare(),
820
+ )
821
+ worker.add(Add)
822
+ signal(
823
+ SIGINT,
824
+ lambda *_: worker.stop(),
825
+ )
826
+ worker.run()
827
+ ```
828
+
829
+ Send `{'a': 10, 'b': 20}` to the same queue from another process.
830
+ The Worker ACKs after execution and completion hooks finish, and applies
831
+ the selected failure policy on error. Omitting `on_error` defaults to `Requeue()`.
832
+
833
+ `Subscription` options:
834
+
835
+ - `queue`: task queue name.
836
+ - `qos`: this subscription's unacknowledged-delivery limit. The consumer default
837
+ is 1; 0 means unlimited. This is separate from shared Worker pool capacity.
838
+ - `error`: error queue to create and bind through `declare()`; a DLQ for rejected messages.
839
+ - `offset`: integer offset for RabbitMQ stream subscriptions.
840
+ - `max_priority`: maximum priority for a classic task queue; an integer in `1..255` or `None`.
841
+ - `priority`: default message priority; an integer in `0..max_priority` or `None`.
842
+ Setting only this value sets `max_priority` to `255`.
843
+
844
+ Constructing a `Subscription` or calling `consumer.subs()` does not declare queues.
845
+ Task `declare()` provisions ordinary durable queues and an optional DLQ,
846
+ not stream queues or Exchange bindings. Incompatible existing queue options
847
+ fail explicitly; queues are not automatically deleted and recreated.
848
+
849
+ ### Worker task priorities
850
+
851
+ Publish tasks with `DefStream.Worker.emit` or `connection.emit(body, parameters)`.
852
+ The task publication API is `emit`. Low-level `send()` and RPC `call()` on queues, exchanges,
853
+ and streams remain unchanged.
854
+
855
+ Set the default `priority` and queue `max_priority` together in the task's
856
+ `Subscription`. There is no separate `priority` setting on `Run`.
857
+
858
+ ```python
859
+ class PriorityAdd(
860
+ Add,
861
+ parameters=Subscription(
862
+ queue='defstream.example.rabbitmq.priority.add',
863
+ qos=1,
864
+ max_priority=5,
865
+ priority=2,
866
+ ),
867
+ ):
868
+ pass
869
+ ```
870
+
871
+ - The Worker API `declare(task=PriorityAdd, broker=connection)` declares the queue
872
+ with `x-max-priority=5` while preserving existing DLQ settings.
873
+ - `emit(..., task=PriorityAdd, broker=connection)` and
874
+ `request(..., task=PriorityAdd, broker=connection)` use the task default of `2`.
875
+ Pass `priority=0` or another valid value to override it for an individual call.
876
+ - Setting only the default sets the queue maximum to `255`. Setting only the
877
+ maximum leaves the default as `None`. Without a per-call value either, the AMQP
878
+ priority property is omitted.
879
+ - Worker calls validate against the task's effective maximum. If priority is
880
+ specified only at call time, only the `0..255` range is validated; the caller
881
+ must provision a priority queue beforehand.
882
+ - The existing low-level `Queue.send`, `Exchange.send`, and `Queue.call` also
883
+ accept `priority=`. These APIs do not receive a task definition and therefore
884
+ do not validate against a task-specific maximum. They accept integers in
885
+ `0..255` and reject `bool`.
886
+ - Larger maximum values increase resource costs; configure only the levels you need.
887
+ - Task definitions declaring the same queue must use the same maximum.
888
+ Redeclaring an ordinary queue or a queue with a different maximum propagates
889
+ a RabbitMQ error. Queues are not automatically deleted. Switch to a new queue,
890
+ or preserve pending messages before safely recreating the queue and migrating
891
+ producers and consumers together.
892
+ - Higher values are delivered first, but prefetched messages and running tasks
893
+ are not preempted. Redelivery and multiple consumers prevent guarantees about
894
+ completion order, and lower-priority messages can wait indefinitely.
895
+ Streams and quorum queues are outside the scope of this feature.
896
+
897
+ ### Synchronous RPC
898
+
899
+ Call a queue served by a running Worker to receive the result of `Run.run()`.
900
+ Use the following between `connect()` and `close()` in the direct-connection example.
901
+
902
+ ```python
903
+ result = connection.queue('defstream.example.rabbitmq.add').call(
904
+ {'a': 10, 'b': 20},
905
+ timeout=5,
906
+ )
907
+ print(result)
908
+ ```
909
+
910
+ - `timeout` is in **seconds**. Missing replies raise `TimeoutError`.
911
+ This does not cancel delivered work or strictly bound total connection and cleanup time.
912
+ - Each call owns a dedicated connection and temporary reply queue, cleaned up on exit.
913
+ - Server rejection raises `DefStream.Broker.RemoteError`.
914
+ Requeue can retry without sending a final error response.
915
+ - Reply publication and request ACK are not atomic. Handle duplicate execution with RPC too.
916
+
917
+ Applications sharing a `Run` class can also use the core helper
918
+ `request(body, task=Add, broker=connection, timeout=5)`.
919
+
920
+ ### Exchanges and streams
921
+
922
+ - `connection.exchange(name)` provides `send()` and `call()`.
923
+ `event=` selects the routing key. Declare the Exchange, queues, and bindings first.
924
+ - Exchange RPC routed to several queues can execute at every destination and
925
+ returns only the first matching reply. Use a single destination for ordinary RPC.
926
+ - Configure routing with `create_exchange()`, `bind_exchange_to_queue()`, and
927
+ `bind_exchange_to_exchange()`. Corresponding `delete_...` or `unbind_...` methods are available.
928
+ - Use `create_stream()` to provision a stream queue, then access
929
+ `connection.stream(name)` with `send()` and `read(offset=0, timeout=1000)`.
930
+ The read timeout is in milliseconds.
931
+ Read events are ACKed automatically when iteration advances.
932
+ - `delete_queue()`, `delete_stream()`, and `delete_exchange()` remove resources;
933
+ use them only for resources owned by your application.
934
+
935
+ ### Operational considerations
936
+
937
+ - Design idempotent tasks for at-least-once delivery.
938
+ - Unlimited requeue can repeatedly execute permanently invalid messages. Plan DLQ and retry policies.
939
+ - Execute long tasks in the Worker's pool rather than on the consumer connection.
940
+ - When publishing from task hooks, create a separate producer connection inside the hook's thread/process.
941
+ - `worker.stop()` waits for in-flight tasks and ACKs before closing the connection and pool.
942
+ There is no forced termination or shutdown timeout.
943
+
944
+ ## Redis usage
945
+
946
+ Requires Python 3.9 or newer and Redis 6.2 or newer. Uses redis-py Streams, not Pub/Sub or list-based queues.
947
+
948
+ ### Send and receive messages
949
+
950
+ This example assumes a local Redis server at `127.0.0.1:6379`.
951
+ It creates the `defstream.example.redis` stream and `DefStream` group and leaves
952
+ them in place.
953
+
954
+ ```python
955
+ from DefStream.Broker.Implements.Redis import Configuration, Connection
956
+
957
+
958
+ connection = Connection(Configuration(
959
+ host='127.0.0.1',
960
+ port=6379,
961
+ ))
962
+ try:
963
+ connection.connect()
964
+ connection.create_queue(
965
+ 'defstream.example.redis',
966
+ group='DefStream',
967
+ )
968
+ queue = connection.queue(
969
+ 'defstream.example.redis',
970
+ group='DefStream',
971
+ )
972
+ message_id = queue.send({'message': 'Hello, DefStream!'})
973
+ print(message_id)
974
+ event = queue.pop(timeout=1000)
975
+ if event is not None:
976
+ print(event.id, event.body)
977
+ event.ack()
978
+ finally:
979
+ connection.close()
980
+ ```
981
+
982
+ `connect()` verifies connectivity with PING and propagates failures.
983
+ The default codec stores the entire message as UTF-8 JSON in a single `body`
984
+ stream field. Legacy field-per-value streams are not compatible.
985
+ Set custom codecs through `Configuration(encode=..., decode=...)`.
986
+
987
+ ### Connection configuration
988
+
989
+ | Option | Default and purpose |
990
+ | --- | --- |
991
+ | `host`, `port` | `127.0.0.1`, `6379` |
992
+ | `username`, `password` | Credentials; password-only authentication is supported |
993
+ | `secure` | Enable TLS; defaults to `False` |
994
+ | `database` | Database number; defaults to 0 |
995
+ | `type` | `ConnectionType.Pool` or `ConnectionType.Cluster` |
996
+ | `max` | Maximum connections; defaults to 10 |
997
+ | `timeout` | Connect/socket timeout in **seconds**; defaults to 5 |
998
+
999
+ Import `ConnectionType` from the adapter package.
1000
+ The default Pool mode uses redis-py's connection pool.
1001
+ Cluster mode uses Redis Cluster and requires `database=0`.
1002
+ Read production credentials from environment variables or a secret store.
1003
+
1004
+ ### Logging
1005
+
1006
+ `Connection`, `Consumer`, `Queue`, and `Event` each use a class-level logger named
1007
+ `DefStream.Broker.Implements.Redis.<ClassName>`. Records propagate to the parent
1008
+ `DefStream` logger and the root logger. The library does not configure handlers
1009
+ or logging levels; configure output destinations and filters in your application.
1010
+ Records exclude message bodies and credentials, and existing exceptions still
1011
+ propagate to callers after an error is logged. Since callers may log the same
1012
+ failure again, keep traceback logging at the boundary responsible for that failure.
1013
+ The examples configure output only at their execution entry points, selecting
1014
+ the `Sample` and Redis logger hierarchies at DEBUG level.
1015
+
1016
+ ### Consumer groups and history
1017
+
1018
+ - `connection.queue(name, group='DefStream')` and `connection.stream(name, group='DefStream')`
1019
+ return the same stream-backed `Queue` type.
1020
+ - `create_queue()` and `create_stream()` prepare the stream and group.
1021
+ Publishing/subscribing can also create the required resources lazily.
1022
+ - New groups start at `0-0`, so messages published before subscription can be consumed.
1023
+ - Consumers in the same group share messages; separate groups each receive them.
1024
+ Preserve the group name to continue an existing deployment's position and pending state.
1025
+ - `pop(timeout=1000)` waits for one group delivery. Its timeout is in **milliseconds**.
1026
+ A positive timeout returns `None` when no delivery arrives; `None` or `0` waits indefinitely.
1027
+
1028
+ Read stream history without changing group state using this snippet between
1029
+ `connect()` and `close()` in the direct-connection example:
1030
+
1031
+ ```python
1032
+ for event in connection.stream('defstream.example.redis').read(
1033
+ offset=0,
1034
+ timeout=1000,
1035
+ ):
1036
+ print(event.id, event.body)
1037
+ ```
1038
+
1039
+ `read()` reads existing history and new messages with a total millisecond timeout.
1040
+ `None` or `0` tails indefinitely. Close the iterator to stop reading.
1041
+ History events are not group deliveries and cannot be ACKed or requeued.
1042
+ A string stream ID as `offset` resumes **after** that ID.
1043
+
1044
+ ### Process tasks with a Worker
1045
+
1046
+ This executable example prepares a task stream and group, then consumes tasks.
1047
+
1048
+ ```python
1049
+ from signal import SIGINT, signal
1050
+
1051
+ from DefStream.Broker.Policies import Reject
1052
+ from DefStream.Broker.Implements.Redis import Configuration, Connection, Subscription
1053
+ from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
1054
+
1055
+
1056
+ class Add(
1057
+ Run,
1058
+ parameters=Subscription(
1059
+ queue='defstream.example.redis.add',
1060
+ group='DefStream',
1061
+ ),
1062
+ on_error=Reject(),
1063
+ ):
1064
+ def run(
1065
+ self,
1066
+ a: int,
1067
+ b: int,
1068
+ ):
1069
+ print(a + b)
1070
+
1071
+
1072
+ class Prepare(WorkerSetup):
1073
+ def __call__(
1074
+ self,
1075
+ worker: Worker,
1076
+ ) -> None:
1077
+ for task in worker.tasks:
1078
+ declare(
1079
+ task=task,
1080
+ broker=worker.broker,
1081
+ )
1082
+
1083
+
1084
+ if __name__ == '__main__':
1085
+ worker = Worker(
1086
+ Connection(Configuration(
1087
+ host='127.0.0.1',
1088
+ port=6379,
1089
+ )),
1090
+ ThreadPool(4),
1091
+ setups=Prepare(),
1092
+ )
1093
+ worker.add(Add)
1094
+ signal(
1095
+ SIGINT,
1096
+ lambda *_: worker.stop(),
1097
+ )
1098
+ worker.run()
1099
+ ```
1100
+
1101
+ Send `{'a': 10, 'b': 20}` to the same stream from another process.
1102
+ The Worker ACKs after execution and completion hooks finish; it does not return
1103
+ the result as an RPC reply. The default failure policy is `Requeue()` if
1104
+ `on_error` is omitted; this example selects `Reject()`.
1105
+ You can also use the task API `emit(body, task=Add, broker=connection)`;
1106
+ the caller must connect and close the connection.
1107
+
1108
+ The task helper and `connection.send(body, parameters)` have been renamed to `emit`.
1109
+ Use `connection.emit(body, parameters)`.
1110
+ Low-level `queue.send(body)` remains unchanged.
1111
+
1112
+ ### ACK, rejection, and retries
1113
+
1114
+ | Operation | Effect |
1115
+ | --- | --- |
1116
+ | `ack()` | XACK for the group; preserves stream history |
1117
+ | `reject()` / `Reject()` | XACK for the group; no built-in DLQ; preserves history |
1118
+ | `nack()` / `Requeue()` | Append a new message and ACK the original pending entry |
1119
+
1120
+ Requeue uses an atomic single-key Lua script, but the copy has a new stream ID.
1121
+ **The new message is visible to every group, not only the failing group.**
1122
+ Use separate retry streams if retries need group isolation.
1123
+ There is no automatic retry-count limit or built-in error stream.
1124
+
1125
+ Direct consumer users can configure:
1126
+
1127
+ | Option | Meaning |
1128
+ | --- | --- |
1129
+ | `connection.consumer(qos=1)` | Total local unacknowledged-delivery limit across subscriptions |
1130
+ | `reclaim_idle=60000` | Idle milliseconds before pending-entry reclamation; defaults to 60 seconds |
1131
+ | `reclaim_idle=None` | Disable automatic pending recovery |
1132
+
1133
+ The default Worker uses the default consumer settings.
1134
+ Another consumer may reclaim a long task while it is still running; plan task
1135
+ duration and recovery together. Shutdown leaves unfinished deliveries pending,
1136
+ without ACK, for later recovery.
1137
+
1138
+ ### Operational considerations
1139
+
1140
+ - Delivery is at least once. Make side effects safe to execute more than once.
1141
+ - ACK/rejection preserves history; retries add entries. Manage retention externally
1142
+ and do not indiscriminately trim pending entries.
1143
+ - `delete_queue()` and `delete_stream()` delete the stream key itself.
1144
+ Do not remove shared streams/groups in cleanup code without ownership.
1145
+ - Connection, decoding, and handler failures propagate; there is no automatic reconnection.
1146
+ - Exchanges and RPC are not supported. Do not use `request()` or console RPC.
1147
+ - Redis Cluster is implemented, but current integration tests cover standalone Redis.
1148
+ - The Worker's connection and pool belong to one run. `worker.stop()` waits for tasks and closes resources.
1149
+
1150
+ ## Broker selection and operational considerations
1151
+
1152
+ | Feature | RabbitMQ | Redis Streams |
1153
+ | --- | --- | --- |
1154
+ | Queue messaging | Supported | Streams and consumer groups |
1155
+ | Exchange routing | Supported | Not supported |
1156
+ | Stream history reads | Supported | Supported |
1157
+ | Task RPC | Supported | Not supported |
1158
+ | Error destination | Explicitly declared DLQ | No built-in destination |
1159
+
1160
+ - Use RabbitMQ connections/resources only on their owning thread, not from task threads sharing the consumer connection.
1161
+ - Redis requires **6.2 or newer**. Consumers in one group share deliveries; different groups each receive the stream.
1162
+ - Redis requeue appends a copy and ACKs the original, making the new message visible to other groups too.
1163
+ - Redis reclaims pending entries after 60 seconds of idle time by default. Another consumer may reclaim an active long task.
1164
+ Direct consumer users can adjust `reclaim_idle` or disable it with `None`; the default Worker uses the default setting.
1165
+ - Redis ACK/rejection preserves history. Manage retention externally and avoid removing pending entries.
1166
+ - Do not assume automatic network reconnection or exactly-once execution.
1167
+ Design application restart, idempotency, retry, and error-queue policies.
1168
+
1169
+ ## License
1170
+
1171
+ DefStream is licensed under the GNU General Public License version 3. You may
1172
+ use, modify, and redistribute the software under its terms. Distributed
1173
+ modified versions must preserve the same license and provide the corresponding
1174
+ source code. The software is provided without warranty.