quantum-framework 0.9.0__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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,715 @@
1
+ """
2
+ RabbitMQ Message Broker Adapter
3
+
4
+ Provides a full-featured AMQP implementation of the MessageBroker interface
5
+ using the pika library.
6
+
7
+ Features:
8
+ - Topic exchanges for pub/sub
9
+ - Direct queues for point-to-point
10
+ - Durable queues and messages
11
+ - Dead letter exchanges
12
+ - Message TTL
13
+ - Consumer prefetch
14
+ - Acknowledgments
15
+
16
+ Requires: pika package (pip install pika)
17
+ """
18
+
19
+ import threading
20
+ import json
21
+ import time
22
+ import uuid
23
+ from typing import Dict, Any, Optional, List
24
+ from dataclasses import dataclass, field
25
+
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ try:
30
+ import pika
31
+ from pika.exceptions import AMQPConnectionError, AMQPChannelError
32
+ PIKA_AVAILABLE = True
33
+ except ImportError:
34
+ PIKA_AVAILABLE = False
35
+ pika = None
36
+ AMQPConnectionError = Exception
37
+ AMQPChannelError = Exception
38
+
39
+ # Support both direct execution and import from parent
40
+ try:
41
+ from quantum.runtime.message_broker import (
42
+ MessageBroker, Message, QueueInfo, MessageHandler,
43
+ MessageBrokerError, ConnectionError, PublishError,
44
+ SubscribeError, QueueError
45
+ )
46
+ except ImportError:
47
+ from message_broker import (
48
+ MessageBroker, Message, QueueInfo, MessageHandler,
49
+ MessageBrokerError, ConnectionError, PublishError,
50
+ SubscribeError, QueueError
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class RabbitSubscription:
56
+ """Internal state for a RabbitMQ subscription"""
57
+ id: str
58
+ pattern: str
59
+ handler: MessageHandler
60
+ is_queue: bool = False
61
+ prefetch: int = 1
62
+ active: bool = True
63
+ consumer_tag: Optional[str] = None
64
+ thread: Optional[threading.Thread] = None
65
+ channel: Any = None
66
+
67
+
68
+ class RabbitMQAdapter(MessageBroker):
69
+ """
70
+ RabbitMQ-based implementation of MessageBroker.
71
+
72
+ Features:
73
+ - Full AMQP 0-9-1 support
74
+ - Durable queues and exchanges
75
+ - Topic exchange for pub/sub with wildcards (* and #)
76
+ - Dead letter exchanges
77
+ - Consumer acknowledgments
78
+ - Request/reply pattern with reply queues
79
+
80
+ Configuration:
81
+ {
82
+ 'host': 'localhost',
83
+ 'port': 5672,
84
+ 'virtual_host': '/',
85
+ 'username': 'guest',
86
+ 'password': 'guest',
87
+ 'heartbeat': 600,
88
+ 'exchange': 'quantum' # Default exchange name
89
+ }
90
+ """
91
+
92
+ def __init__(self):
93
+ if not PIKA_AVAILABLE:
94
+ raise ImportError(
95
+ "RabbitMQ adapter requires 'pika' package. "
96
+ "Install with: pip install pika"
97
+ )
98
+
99
+ self._connection: Any = None
100
+ self._channel: Any = None
101
+ self._connected = False
102
+ self._config: Dict[str, Any] = {}
103
+ self._exchange = 'quantum'
104
+
105
+ self._lock = threading.RLock()
106
+ self._subscriptions: Dict[str, RabbitSubscription] = {}
107
+ self._pending_acks: Dict[str, Any] = {} # message_id -> delivery_tag
108
+ self._reply_queues: Dict[str, Any] = {}
109
+
110
+ self._shutdown = threading.Event()
111
+
112
+ def connect(self, config: Dict[str, Any] = None) -> None:
113
+ """
114
+ Connect to RabbitMQ.
115
+
116
+ Args:
117
+ config: RabbitMQ connection configuration
118
+ """
119
+ if config is None:
120
+ config = {}
121
+
122
+ self._config = config
123
+ self._exchange = config.get('exchange', 'quantum')
124
+
125
+ try:
126
+ credentials = pika.PlainCredentials(
127
+ config.get('username', 'guest'),
128
+ config.get('password', 'guest')
129
+ )
130
+
131
+ parameters = pika.ConnectionParameters(
132
+ host=config.get('host', 'localhost'),
133
+ port=config.get('port', 5672),
134
+ virtual_host=config.get('virtual_host', '/'),
135
+ credentials=credentials,
136
+ heartbeat=config.get('heartbeat', 600)
137
+ )
138
+
139
+ self._connection = pika.BlockingConnection(parameters)
140
+ self._channel = self._connection.channel()
141
+
142
+ # Declare the main topic exchange
143
+ self._channel.exchange_declare(
144
+ exchange=self._exchange,
145
+ exchange_type='topic',
146
+ durable=True
147
+ )
148
+
149
+ self._connected = True
150
+ self._shutdown.clear()
151
+
152
+ except AMQPConnectionError as e:
153
+ raise ConnectionError(f"Failed to connect to RabbitMQ: {e}")
154
+
155
+ def disconnect(self) -> None:
156
+ """Disconnect from RabbitMQ and clean up resources."""
157
+ with self._lock:
158
+ if not self._connected:
159
+ return
160
+
161
+ self._shutdown.set()
162
+
163
+ # Stop all subscriptions
164
+ for sub in list(self._subscriptions.values()):
165
+ sub.active = False
166
+ if sub.consumer_tag and sub.channel:
167
+ try:
168
+ sub.channel.basic_cancel(sub.consumer_tag)
169
+ except Exception:
170
+ pass
171
+ if sub.thread and sub.thread.is_alive():
172
+ sub.thread.join(timeout=1.0)
173
+
174
+ self._subscriptions.clear()
175
+
176
+ # Close channel and connection
177
+ try:
178
+ if self._channel and self._channel.is_open:
179
+ self._channel.close()
180
+ except Exception:
181
+ pass
182
+
183
+ try:
184
+ if self._connection and self._connection.is_open:
185
+ self._connection.close()
186
+ except Exception:
187
+ pass
188
+
189
+ self._channel = None
190
+ self._connection = None
191
+ self._connected = False
192
+
193
+ def is_connected(self) -> bool:
194
+ """Check if connected to RabbitMQ."""
195
+ return (
196
+ self._connected and
197
+ self._connection is not None and
198
+ self._connection.is_open and
199
+ self._channel is not None and
200
+ self._channel.is_open
201
+ )
202
+
203
+ def publish(self, topic: str, message: Message) -> None:
204
+ """
205
+ Publish a message to a topic.
206
+
207
+ Uses the topic exchange with routing key = topic.
208
+
209
+ Args:
210
+ topic: Topic name (routing key)
211
+ message: Message to publish
212
+ """
213
+ if not self.is_connected():
214
+ raise ConnectionError("Not connected to RabbitMQ")
215
+
216
+ message.topic = topic
217
+
218
+ try:
219
+ properties = pika.BasicProperties(
220
+ message_id=message.id,
221
+ timestamp=int(message.timestamp),
222
+ headers=message.headers,
223
+ content_type='application/json',
224
+ delivery_mode=2 # Persistent
225
+ )
226
+
227
+ self._channel.basic_publish(
228
+ exchange=self._exchange,
229
+ routing_key=topic,
230
+ body=message.to_json(),
231
+ properties=properties
232
+ )
233
+
234
+ except AMQPChannelError as e:
235
+ raise PublishError(f"Failed to publish message: {e}")
236
+
237
+ def send(self, queue_name: str, message: Message) -> None:
238
+ """
239
+ Send a message directly to a queue.
240
+
241
+ Args:
242
+ queue_name: Queue name
243
+ message: Message to send
244
+ """
245
+ if not self.is_connected():
246
+ raise ConnectionError("Not connected to RabbitMQ")
247
+
248
+ message.queue = queue_name
249
+
250
+ try:
251
+ properties = pika.BasicProperties(
252
+ message_id=message.id,
253
+ timestamp=int(message.timestamp),
254
+ headers=message.headers,
255
+ content_type='application/json',
256
+ delivery_mode=2, # Persistent
257
+ reply_to=message.reply_to,
258
+ correlation_id=message.correlation_id
259
+ )
260
+
261
+ self._channel.basic_publish(
262
+ exchange='', # Default exchange
263
+ routing_key=queue_name,
264
+ body=message.to_json(),
265
+ properties=properties
266
+ )
267
+
268
+ except AMQPChannelError as e:
269
+ raise PublishError(f"Failed to send message: {e}")
270
+
271
+ def request(self, queue_name: str, message: Message, timeout: int = 5000) -> Message:
272
+ """
273
+ Send a request and wait for a reply.
274
+
275
+ Args:
276
+ queue_name: Queue to send request to
277
+ message: Request message
278
+ timeout: Timeout in milliseconds
279
+
280
+ Returns:
281
+ Response message
282
+ """
283
+ if not self.is_connected():
284
+ raise ConnectionError("Not connected to RabbitMQ")
285
+
286
+ # Create exclusive reply queue
287
+ result = self._channel.queue_declare(queue='', exclusive=True)
288
+ reply_queue = result.method.queue
289
+ correlation_id = str(uuid.uuid4())
290
+
291
+ message.reply_to = reply_queue
292
+ message.correlation_id = correlation_id
293
+
294
+ response = [None]
295
+ response_received = threading.Event()
296
+
297
+ def on_response(ch, method, props, body):
298
+ if props.correlation_id == correlation_id:
299
+ response[0] = Message.from_json(body.decode())
300
+ response_received.set()
301
+
302
+ try:
303
+ self._channel.basic_consume(
304
+ queue=reply_queue,
305
+ on_message_callback=on_response,
306
+ auto_ack=True
307
+ )
308
+
309
+ # Send request
310
+ self.send(queue_name, message)
311
+
312
+ # Wait for response
313
+ start_time = time.time()
314
+ while not response_received.is_set():
315
+ self._connection.process_data_events(time_limit=0.1)
316
+ if time.time() - start_time > timeout / 1000.0:
317
+ raise TimeoutError(f"Request timed out after {timeout}ms")
318
+
319
+ return response[0]
320
+
321
+ finally:
322
+ # Clean up reply queue
323
+ try:
324
+ self._channel.queue_delete(queue=reply_queue)
325
+ except Exception:
326
+ pass
327
+
328
+ def subscribe(self, topic: str, handler: MessageHandler) -> str:
329
+ """
330
+ Subscribe to a topic pattern.
331
+
332
+ Args:
333
+ topic: Topic pattern (supports * and # wildcards)
334
+ handler: Callback for received messages
335
+
336
+ Returns:
337
+ Subscription ID
338
+ """
339
+ if not self.is_connected():
340
+ raise ConnectionError("Not connected to RabbitMQ")
341
+
342
+ sub_id = f"sub_{uuid.uuid4().hex}"
343
+
344
+ try:
345
+ # Create exclusive queue for this subscription
346
+ result = self._channel.queue_declare(queue='', exclusive=True)
347
+ queue_name = result.method.queue
348
+
349
+ # Bind to topic exchange
350
+ self._channel.queue_bind(
351
+ exchange=self._exchange,
352
+ queue=queue_name,
353
+ routing_key=topic
354
+ )
355
+
356
+ subscription = RabbitSubscription(
357
+ id=sub_id,
358
+ pattern=topic,
359
+ handler=handler,
360
+ is_queue=False
361
+ )
362
+
363
+ # Start consumer in separate thread
364
+ thread = threading.Thread(
365
+ target=self._consume_thread,
366
+ args=(subscription, queue_name, True),
367
+ daemon=True,
368
+ name=f"RabbitMQ-Sub-{sub_id}"
369
+ )
370
+ subscription.thread = thread
371
+
372
+ with self._lock:
373
+ self._subscriptions[sub_id] = subscription
374
+
375
+ thread.start()
376
+
377
+ return sub_id
378
+
379
+ except AMQPChannelError as e:
380
+ raise SubscribeError(f"Failed to subscribe: {e}")
381
+
382
+ def consume(self, queue_name: str, handler: MessageHandler, prefetch: int = 1) -> str:
383
+ """
384
+ Start consuming messages from a queue.
385
+
386
+ Args:
387
+ queue_name: Queue name
388
+ handler: Callback for received messages
389
+ prefetch: Number of messages to prefetch
390
+
391
+ Returns:
392
+ Consumer ID
393
+ """
394
+ if not self.is_connected():
395
+ raise ConnectionError("Not connected to RabbitMQ")
396
+
397
+ consumer_id = f"consumer_{uuid.uuid4().hex}"
398
+
399
+ subscription = RabbitSubscription(
400
+ id=consumer_id,
401
+ pattern=queue_name,
402
+ handler=handler,
403
+ is_queue=True,
404
+ prefetch=prefetch
405
+ )
406
+
407
+ # Start consumer in separate thread
408
+ thread = threading.Thread(
409
+ target=self._consume_thread,
410
+ args=(subscription, queue_name, False),
411
+ daemon=True,
412
+ name=f"RabbitMQ-Consumer-{consumer_id}"
413
+ )
414
+ subscription.thread = thread
415
+
416
+ with self._lock:
417
+ self._subscriptions[consumer_id] = subscription
418
+
419
+ thread.start()
420
+
421
+ return consumer_id
422
+
423
+ def unsubscribe(self, subscription_id: str) -> None:
424
+ """
425
+ Unsubscribe or stop consuming.
426
+
427
+ Args:
428
+ subscription_id: Subscription or consumer ID
429
+ """
430
+ with self._lock:
431
+ if subscription_id in self._subscriptions:
432
+ sub = self._subscriptions[subscription_id]
433
+ sub.active = False
434
+
435
+ if sub.consumer_tag and sub.channel:
436
+ try:
437
+ sub.channel.basic_cancel(sub.consumer_tag)
438
+ except Exception:
439
+ pass
440
+
441
+ if sub.thread and sub.thread.is_alive():
442
+ sub.thread.join(timeout=1.0)
443
+
444
+ del self._subscriptions[subscription_id]
445
+
446
+ def ack(self, message: Message) -> None:
447
+ """
448
+ Acknowledge message processing.
449
+
450
+ Args:
451
+ message: Message to acknowledge
452
+ """
453
+ with self._lock:
454
+ delivery_tag = self._pending_acks.pop(message.id, None)
455
+ if delivery_tag:
456
+ try:
457
+ self._channel.basic_ack(delivery_tag=delivery_tag)
458
+ except Exception:
459
+ pass
460
+
461
+ def nack(self, message: Message, requeue: bool = True) -> None:
462
+ """
463
+ Negative acknowledge - reject a message.
464
+
465
+ Args:
466
+ message: Message to reject
467
+ requeue: If True, requeue; if False, send to DLX
468
+ """
469
+ with self._lock:
470
+ delivery_tag = self._pending_acks.pop(message.id, None)
471
+ if delivery_tag:
472
+ try:
473
+ self._channel.basic_nack(
474
+ delivery_tag=delivery_tag,
475
+ requeue=requeue
476
+ )
477
+ except Exception:
478
+ pass
479
+
480
+ def declare_queue(
481
+ self,
482
+ name: str,
483
+ durable: bool = True,
484
+ exclusive: bool = False,
485
+ auto_delete: bool = False,
486
+ dead_letter_queue: Optional[str] = None,
487
+ ttl: Optional[int] = None
488
+ ) -> None:
489
+ """
490
+ Declare a queue.
491
+
492
+ Args:
493
+ name: Queue name
494
+ durable: Survive broker restart
495
+ exclusive: Only this connection can access
496
+ auto_delete: Delete when last consumer disconnects
497
+ dead_letter_queue: Queue for rejected messages
498
+ ttl: Message TTL in milliseconds
499
+ """
500
+ if not self.is_connected():
501
+ raise ConnectionError("Not connected to RabbitMQ")
502
+
503
+ try:
504
+ arguments = {}
505
+
506
+ if dead_letter_queue:
507
+ # Create dead letter exchange
508
+ dlx_name = f"{name}_dlx"
509
+ self._channel.exchange_declare(
510
+ exchange=dlx_name,
511
+ exchange_type='direct',
512
+ durable=True
513
+ )
514
+
515
+ # Create dead letter queue
516
+ self._channel.queue_declare(
517
+ queue=dead_letter_queue,
518
+ durable=True
519
+ )
520
+
521
+ # Bind DLQ to DLX
522
+ self._channel.queue_bind(
523
+ exchange=dlx_name,
524
+ queue=dead_letter_queue,
525
+ routing_key=dead_letter_queue
526
+ )
527
+
528
+ arguments['x-dead-letter-exchange'] = dlx_name
529
+ arguments['x-dead-letter-routing-key'] = dead_letter_queue
530
+
531
+ if ttl:
532
+ arguments['x-message-ttl'] = ttl
533
+
534
+ self._channel.queue_declare(
535
+ queue=name,
536
+ durable=durable,
537
+ exclusive=exclusive,
538
+ auto_delete=auto_delete,
539
+ arguments=arguments if arguments else None
540
+ )
541
+
542
+ except AMQPChannelError as e:
543
+ raise QueueError(f"Failed to declare queue: {e}")
544
+
545
+ def purge_queue(self, name: str) -> int:
546
+ """
547
+ Remove all messages from a queue.
548
+
549
+ Args:
550
+ name: Queue name
551
+
552
+ Returns:
553
+ Number of messages purged
554
+ """
555
+ if not self.is_connected():
556
+ raise ConnectionError("Not connected to RabbitMQ")
557
+
558
+ try:
559
+ result = self._channel.queue_purge(queue=name)
560
+ return result.method.message_count
561
+ except AMQPChannelError as e:
562
+ raise QueueError(f"Failed to purge queue: {e}")
563
+
564
+ def delete_queue(self, name: str) -> None:
565
+ """
566
+ Delete a queue.
567
+
568
+ Args:
569
+ name: Queue name
570
+ """
571
+ if not self.is_connected():
572
+ raise ConnectionError("Not connected to RabbitMQ")
573
+
574
+ try:
575
+ self._channel.queue_delete(queue=name)
576
+ except AMQPChannelError as e:
577
+ raise QueueError(f"Failed to delete queue: {e}")
578
+
579
+ def get_queue_info(self, name: str) -> QueueInfo:
580
+ """
581
+ Get queue statistics.
582
+
583
+ Args:
584
+ name: Queue name
585
+
586
+ Returns:
587
+ QueueInfo with statistics
588
+ """
589
+ if not self.is_connected():
590
+ raise ConnectionError("Not connected to RabbitMQ")
591
+
592
+ try:
593
+ result = self._channel.queue_declare(
594
+ queue=name,
595
+ passive=True # Only check, don't create
596
+ )
597
+
598
+ return QueueInfo(
599
+ name=name,
600
+ message_count=result.method.message_count,
601
+ consumer_count=result.method.consumer_count,
602
+ durable=True, # Cannot determine from passive declare
603
+ auto_delete=False
604
+ )
605
+
606
+ except AMQPChannelError as e:
607
+ raise QueueError(f"Queue '{name}' not found: {e}")
608
+
609
+ def _consume_thread(
610
+ self,
611
+ subscription: RabbitSubscription,
612
+ queue_name: str,
613
+ auto_ack: bool
614
+ ) -> None:
615
+ """Background thread for consuming messages."""
616
+ try:
617
+ # Create new connection for this thread
618
+ credentials = pika.PlainCredentials(
619
+ self._config.get('username', 'guest'),
620
+ self._config.get('password', 'guest')
621
+ )
622
+
623
+ parameters = pika.ConnectionParameters(
624
+ host=self._config.get('host', 'localhost'),
625
+ port=self._config.get('port', 5672),
626
+ virtual_host=self._config.get('virtual_host', '/'),
627
+ credentials=credentials,
628
+ heartbeat=self._config.get('heartbeat', 600)
629
+ )
630
+
631
+ connection = pika.BlockingConnection(parameters)
632
+ channel = connection.channel()
633
+ subscription.channel = channel
634
+
635
+ # Set prefetch
636
+ channel.basic_qos(prefetch_count=subscription.prefetch)
637
+
638
+ def callback(ch, method, properties, body):
639
+ if not subscription.active:
640
+ return
641
+
642
+ try:
643
+ msg = Message.from_json(body.decode())
644
+
645
+ if not auto_ack:
646
+ with self._lock:
647
+ self._pending_acks[msg.id] = method.delivery_tag
648
+
649
+ subscription.handler(msg)
650
+
651
+ if auto_ack:
652
+ ch.basic_ack(delivery_tag=method.delivery_tag)
653
+
654
+ except Exception as e:
655
+ print(f"[RabbitMQ] Handler error: {e}")
656
+ if not auto_ack:
657
+ ch.basic_nack(
658
+ delivery_tag=method.delivery_tag,
659
+ requeue=True
660
+ )
661
+
662
+ consumer_tag = channel.basic_consume(
663
+ queue=queue_name,
664
+ on_message_callback=callback,
665
+ auto_ack=False
666
+ )
667
+ subscription.consumer_tag = consumer_tag
668
+
669
+ while subscription.active and not self._shutdown.is_set():
670
+ try:
671
+ connection.process_data_events(time_limit=1.0)
672
+ except Exception:
673
+ if subscription.active:
674
+ time.sleep(1)
675
+
676
+ # Cleanup
677
+ try:
678
+ channel.close()
679
+ connection.close()
680
+ except Exception:
681
+ pass
682
+
683
+ except Exception as e:
684
+ print(f"[RabbitMQ] Consumer thread error: {e}")
685
+
686
+ def reply(self, original_message: Message, response: Message) -> None:
687
+ """
688
+ Send a reply to a request message.
689
+
690
+ Args:
691
+ original_message: The request message
692
+ response: The response message
693
+ """
694
+ if not original_message.reply_to:
695
+ raise PublishError("Original message has no reply_to queue")
696
+
697
+ response.correlation_id = original_message.correlation_id
698
+ response.queue = original_message.reply_to
699
+
700
+ try:
701
+ properties = pika.BasicProperties(
702
+ message_id=response.id,
703
+ correlation_id=response.correlation_id,
704
+ content_type='application/json'
705
+ )
706
+
707
+ self._channel.basic_publish(
708
+ exchange='',
709
+ routing_key=original_message.reply_to,
710
+ body=response.to_json(),
711
+ properties=properties
712
+ )
713
+
714
+ except AMQPChannelError as e:
715
+ raise PublishError(f"Failed to send reply: {e}")