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,690 @@
1
+ """
2
+ In-Memory Message Broker Adapter
3
+
4
+ Provides a simple in-memory implementation of the MessageBroker interface
5
+ for testing and development purposes.
6
+
7
+ Features:
8
+ - Topic-based pub/sub with pattern matching
9
+ - Queue-based point-to-point messaging
10
+ - Request/reply pattern support
11
+ - Message acknowledgment (simulated)
12
+ - No external dependencies
13
+ """
14
+
15
+ import threading
16
+ import queue
17
+ import fnmatch
18
+ import time
19
+ import uuid
20
+ from typing import Dict, Any, Optional, List, Callable
21
+ from collections import defaultdict
22
+ from dataclasses import dataclass, field
23
+
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ # Support both direct execution and import from parent
28
+ try:
29
+ from quantum.runtime.message_broker import (
30
+ MessageBroker, Message, QueueInfo, MessageHandler,
31
+ MessageBrokerError, ConnectionError, PublishError,
32
+ SubscribeError, QueueError
33
+ )
34
+ except ImportError:
35
+ from message_broker import (
36
+ MessageBroker, Message, QueueInfo, MessageHandler,
37
+ MessageBrokerError, ConnectionError, PublishError,
38
+ SubscribeError, QueueError
39
+ )
40
+
41
+
42
+ @dataclass
43
+ class QueueState:
44
+ """Internal state for a queue"""
45
+ messages: queue.Queue = field(default_factory=queue.Queue)
46
+ durable: bool = True
47
+ exclusive: bool = False
48
+ auto_delete: bool = False
49
+ dead_letter_queue: Optional[str] = None
50
+ ttl: Optional[int] = None
51
+ consumers: List[str] = field(default_factory=list)
52
+
53
+
54
+ @dataclass
55
+ class Subscription:
56
+ """Internal state for a subscription"""
57
+ id: str
58
+ pattern: str # topic pattern or queue name
59
+ handler: MessageHandler
60
+ is_queue: bool = False
61
+ prefetch: int = 1
62
+ active: bool = True
63
+
64
+
65
+ class MemoryAdapter(MessageBroker):
66
+ """
67
+ In-memory implementation of MessageBroker.
68
+
69
+ Suitable for:
70
+ - Unit testing
71
+ - Development environments
72
+ - Single-process applications
73
+ - Prototyping
74
+
75
+ Limitations:
76
+ - Messages are lost on process restart
77
+ - No persistence
78
+ - Single process only (no IPC)
79
+ """
80
+
81
+ def __init__(self):
82
+ self._connected = False
83
+ self._lock = threading.RLock()
84
+
85
+ # Queues: name -> QueueState
86
+ self._queues: Dict[str, QueueState] = {}
87
+
88
+ # Topic subscriptions: subscription_id -> Subscription
89
+ self._subscriptions: Dict[str, Subscription] = {}
90
+
91
+ # Pending acknowledgments: message_id -> Message
92
+ self._pending_acks: Dict[str, Message] = {}
93
+
94
+ # Reply queues for request/reply pattern
95
+ self._reply_queues: Dict[str, queue.Queue] = {}
96
+
97
+ # Background worker threads
98
+ self._workers: List[threading.Thread] = []
99
+ self._shutdown = threading.Event()
100
+
101
+ def connect(self, config: Dict[str, Any] = None) -> None:
102
+ """
103
+ Initialize the in-memory broker.
104
+
105
+ Args:
106
+ config: Optional configuration (ignored for memory adapter)
107
+ """
108
+ with self._lock:
109
+ if self._connected:
110
+ return
111
+
112
+ self._connected = True
113
+ self._shutdown.clear()
114
+
115
+ # Start background worker for queue consumption
116
+ worker = threading.Thread(
117
+ target=self._consume_worker,
118
+ daemon=True,
119
+ name="MemoryBroker-Worker"
120
+ )
121
+ worker.start()
122
+ self._workers.append(worker)
123
+
124
+ def disconnect(self) -> None:
125
+ """Shutdown the in-memory broker and clean up resources."""
126
+ with self._lock:
127
+ if not self._connected:
128
+ return
129
+
130
+ # Signal workers to stop
131
+ self._shutdown.set()
132
+
133
+ # Wait for workers to finish
134
+ for worker in self._workers:
135
+ worker.join(timeout=1.0)
136
+
137
+ self._workers.clear()
138
+
139
+ # Clear all state
140
+ self._queues.clear()
141
+ self._subscriptions.clear()
142
+ self._pending_acks.clear()
143
+ self._reply_queues.clear()
144
+
145
+ self._connected = False
146
+
147
+ def is_connected(self) -> bool:
148
+ """Check if broker is initialized."""
149
+ return self._connected
150
+
151
+ def publish(self, topic: str, message: Message) -> None:
152
+ """
153
+ Publish a message to a topic.
154
+
155
+ All matching subscriptions will receive the message.
156
+
157
+ Args:
158
+ topic: Topic name
159
+ message: Message to publish
160
+ """
161
+ if not self._connected:
162
+ raise ConnectionError("Broker not connected")
163
+
164
+ message.topic = topic
165
+
166
+ with self._lock:
167
+ # Find all matching subscriptions
168
+ for sub in self._subscriptions.values():
169
+ if sub.active and not sub.is_queue:
170
+ if self._match_topic(topic, sub.pattern):
171
+ try:
172
+ # Create a copy of the message for each subscriber
173
+ sub.handler(Message.from_dict(message.to_dict()))
174
+ except Exception as e:
175
+ # Log error but don't fail publish
176
+ print(f"[MemoryBroker] Handler error: {e}")
177
+
178
+ def send(self, queue_name: str, message: Message) -> None:
179
+ """
180
+ Send a message to a queue.
181
+
182
+ Args:
183
+ queue_name: Queue name
184
+ message: Message to send
185
+ """
186
+ if not self._connected:
187
+ raise ConnectionError("Broker not connected")
188
+
189
+ message.queue = queue_name
190
+
191
+ with self._lock:
192
+ # Ensure queue exists
193
+ if queue_name not in self._queues:
194
+ self._queues[queue_name] = QueueState()
195
+
196
+ q = self._queues[queue_name]
197
+
198
+ # Check TTL
199
+ if q.ttl is not None:
200
+ message.headers['_expires'] = str(time.time() * 1000 + q.ttl)
201
+
202
+ q.messages.put(message)
203
+
204
+ def request(self, queue_name: str, message: Message, timeout: int = 5000) -> Message:
205
+ """
206
+ Send a request and wait for a reply.
207
+
208
+ Args:
209
+ queue_name: Queue to send request to
210
+ message: Request message
211
+ timeout: Timeout in milliseconds
212
+
213
+ Returns:
214
+ Response message
215
+
216
+ Raises:
217
+ TimeoutError: If no response within timeout
218
+ """
219
+ if not self._connected:
220
+ raise ConnectionError("Broker not connected")
221
+
222
+ # Create unique reply queue
223
+ reply_queue_name = f"_reply_{uuid.uuid4().hex}"
224
+ correlation_id = str(uuid.uuid4())
225
+
226
+ # Set up reply queue
227
+ reply_queue = queue.Queue()
228
+ with self._lock:
229
+ self._reply_queues[reply_queue_name] = reply_queue
230
+
231
+ try:
232
+ # Set message properties for reply
233
+ message.reply_to = reply_queue_name
234
+ message.correlation_id = correlation_id
235
+
236
+ # Send request
237
+ self.send(queue_name, message)
238
+
239
+ # Wait for reply
240
+ try:
241
+ reply = reply_queue.get(timeout=timeout / 1000.0)
242
+ return reply
243
+ except queue.Empty:
244
+ raise TimeoutError(f"Request timed out after {timeout}ms")
245
+
246
+ finally:
247
+ # Clean up reply queue
248
+ with self._lock:
249
+ self._reply_queues.pop(reply_queue_name, None)
250
+
251
+ def subscribe(self, topic: str, handler: MessageHandler) -> str:
252
+ """
253
+ Subscribe to a topic pattern.
254
+
255
+ Args:
256
+ topic: Topic pattern (supports * and # wildcards)
257
+ handler: Callback for received messages
258
+
259
+ Returns:
260
+ Subscription ID
261
+ """
262
+ if not self._connected:
263
+ raise ConnectionError("Broker not connected")
264
+
265
+ sub_id = f"sub_{uuid.uuid4().hex}"
266
+
267
+ with self._lock:
268
+ self._subscriptions[sub_id] = Subscription(
269
+ id=sub_id,
270
+ pattern=topic,
271
+ handler=handler,
272
+ is_queue=False
273
+ )
274
+
275
+ return sub_id
276
+
277
+ def consume(self, queue_name: str, handler: MessageHandler, prefetch: int = 1) -> str:
278
+ """
279
+ Start consuming messages from a queue.
280
+
281
+ Args:
282
+ queue_name: Queue name
283
+ handler: Callback for received messages
284
+ prefetch: Number of messages to prefetch
285
+
286
+ Returns:
287
+ Consumer ID
288
+ """
289
+ if not self._connected:
290
+ raise ConnectionError("Broker not connected")
291
+
292
+ consumer_id = f"consumer_{uuid.uuid4().hex}"
293
+
294
+ with self._lock:
295
+ # Ensure queue exists
296
+ if queue_name not in self._queues:
297
+ self._queues[queue_name] = QueueState()
298
+
299
+ self._queues[queue_name].consumers.append(consumer_id)
300
+
301
+ self._subscriptions[consumer_id] = Subscription(
302
+ id=consumer_id,
303
+ pattern=queue_name,
304
+ handler=handler,
305
+ is_queue=True,
306
+ prefetch=prefetch
307
+ )
308
+
309
+ return consumer_id
310
+
311
+ def unsubscribe(self, subscription_id: str) -> None:
312
+ """
313
+ Unsubscribe from a topic or stop consuming.
314
+
315
+ Args:
316
+ subscription_id: Subscription or consumer ID
317
+ """
318
+ with self._lock:
319
+ if subscription_id in self._subscriptions:
320
+ sub = self._subscriptions[subscription_id]
321
+ sub.active = False
322
+
323
+ # Remove from queue consumers if applicable
324
+ if sub.is_queue and sub.pattern in self._queues:
325
+ consumers = self._queues[sub.pattern].consumers
326
+ if subscription_id in consumers:
327
+ consumers.remove(subscription_id)
328
+
329
+ del self._subscriptions[subscription_id]
330
+
331
+ def ack(self, message: Message) -> None:
332
+ """
333
+ Acknowledge message processing.
334
+
335
+ Args:
336
+ message: Message to acknowledge
337
+ """
338
+ with self._lock:
339
+ self._pending_acks.pop(message.id, None)
340
+
341
+ def nack(self, message: Message, requeue: bool = True) -> None:
342
+ """
343
+ Negative acknowledge - reject a message.
344
+
345
+ Args:
346
+ message: Message to reject
347
+ requeue: If True, requeue; if False, send to DLQ
348
+ """
349
+ with self._lock:
350
+ self._pending_acks.pop(message.id, None)
351
+
352
+ if message.queue:
353
+ q = self._queues.get(message.queue)
354
+
355
+ if requeue and q:
356
+ # Requeue the message
357
+ q.messages.put(message)
358
+ elif q and q.dead_letter_queue:
359
+ # Send to dead letter queue
360
+ if q.dead_letter_queue not in self._queues:
361
+ self._queues[q.dead_letter_queue] = QueueState()
362
+ self._queues[q.dead_letter_queue].messages.put(message)
363
+
364
+ def declare_queue(
365
+ self,
366
+ name: str,
367
+ durable: bool = True,
368
+ exclusive: bool = False,
369
+ auto_delete: bool = False,
370
+ dead_letter_queue: Optional[str] = None,
371
+ ttl: Optional[int] = None
372
+ ) -> None:
373
+ """
374
+ Declare a queue.
375
+
376
+ Args:
377
+ name: Queue name
378
+ durable: (ignored in memory adapter)
379
+ exclusive: If True, only this connection can access
380
+ auto_delete: If True, delete when last consumer disconnects
381
+ dead_letter_queue: Queue for rejected messages
382
+ ttl: Message TTL in milliseconds
383
+ """
384
+ if not self._connected:
385
+ raise ConnectionError("Broker not connected")
386
+
387
+ with self._lock:
388
+ if name not in self._queues:
389
+ self._queues[name] = QueueState(
390
+ durable=durable,
391
+ exclusive=exclusive,
392
+ auto_delete=auto_delete,
393
+ dead_letter_queue=dead_letter_queue,
394
+ ttl=ttl
395
+ )
396
+
397
+ # Also declare DLQ if specified
398
+ if dead_letter_queue and dead_letter_queue not in self._queues:
399
+ self._queues[dead_letter_queue] = QueueState(durable=True)
400
+
401
+ def purge_queue(self, name: str) -> int:
402
+ """
403
+ Remove all messages from a queue.
404
+
405
+ Args:
406
+ name: Queue name
407
+
408
+ Returns:
409
+ Number of messages purged
410
+ """
411
+ if not self._connected:
412
+ raise ConnectionError("Broker not connected")
413
+
414
+ with self._lock:
415
+ if name not in self._queues:
416
+ raise QueueError(f"Queue '{name}' not found")
417
+
418
+ q = self._queues[name]
419
+ count = q.messages.qsize()
420
+
421
+ # Clear the queue
422
+ while not q.messages.empty():
423
+ try:
424
+ q.messages.get_nowait()
425
+ except queue.Empty:
426
+ break
427
+
428
+ return count
429
+
430
+ def delete_queue(self, name: str) -> None:
431
+ """
432
+ Delete a queue.
433
+
434
+ Args:
435
+ name: Queue name
436
+ """
437
+ if not self._connected:
438
+ raise ConnectionError("Broker not connected")
439
+
440
+ with self._lock:
441
+ if name in self._queues:
442
+ del self._queues[name]
443
+
444
+ def get_queue_info(self, name: str) -> QueueInfo:
445
+ """
446
+ Get queue statistics.
447
+
448
+ Args:
449
+ name: Queue name
450
+
451
+ Returns:
452
+ QueueInfo with statistics
453
+ """
454
+ if not self._connected:
455
+ raise ConnectionError("Broker not connected")
456
+
457
+ with self._lock:
458
+ if name not in self._queues:
459
+ raise QueueError(f"Queue '{name}' not found")
460
+
461
+ q = self._queues[name]
462
+ return QueueInfo(
463
+ name=name,
464
+ message_count=q.messages.qsize(),
465
+ consumer_count=len(q.consumers),
466
+ durable=q.durable,
467
+ auto_delete=q.auto_delete
468
+ )
469
+
470
+ def _match_topic(self, topic: str, pattern: str) -> bool:
471
+ """
472
+ Match a topic against a pattern.
473
+
474
+ Supports MQTT-style wildcards:
475
+ - * matches exactly one level
476
+ - # matches zero or more levels
477
+
478
+ Args:
479
+ topic: Actual topic name
480
+ pattern: Pattern to match against
481
+
482
+ Returns:
483
+ True if topic matches pattern
484
+ """
485
+ if pattern == topic:
486
+ return True
487
+
488
+ # Convert MQTT-style wildcards to fnmatch
489
+ # * -> [^.]* (match one level)
490
+ # # -> * (match any)
491
+ if '#' in pattern or '*' in pattern:
492
+ # Simple wildcard matching
493
+ regex_pattern = pattern.replace('.', r'\.').replace('#', '*').replace('*', '[^.]*')
494
+ import re
495
+ return bool(re.match(f'^{regex_pattern}$', topic))
496
+
497
+ return False
498
+
499
+ def _consume_worker(self) -> None:
500
+ """Background worker for delivering queue messages to consumers."""
501
+ while not self._shutdown.is_set():
502
+ try:
503
+ with self._lock:
504
+ # Find all queue subscriptions
505
+ queue_subs = [
506
+ s for s in self._subscriptions.values()
507
+ if s.is_queue and s.active
508
+ ]
509
+
510
+ for sub in queue_subs:
511
+ queue_name = sub.pattern
512
+ with self._lock:
513
+ if queue_name not in self._queues:
514
+ continue
515
+
516
+ q = self._queues[queue_name]
517
+ if q.messages.empty():
518
+ continue
519
+
520
+ # Get message
521
+ try:
522
+ msg = q.messages.get_nowait()
523
+ except queue.Empty:
524
+ continue
525
+
526
+ # Check if message expired
527
+ expires = msg.headers.get('_expires')
528
+ if expires and float(expires) < time.time() * 1000:
529
+ # Message expired - send to DLQ
530
+ with self._lock:
531
+ if q.dead_letter_queue:
532
+ dlq = self._queues.get(q.dead_letter_queue)
533
+ if dlq:
534
+ dlq.messages.put(msg)
535
+ continue
536
+
537
+ # Track pending ack
538
+ with self._lock:
539
+ self._pending_acks[msg.id] = msg
540
+
541
+ # Call handler
542
+ try:
543
+ sub.handler(msg)
544
+
545
+ # Auto-ack if not in pending (handler didn't manually ack)
546
+ # In real implementation, this would depend on ack mode
547
+ except Exception as e:
548
+ print(f"[MemoryBroker] Consumer error: {e}")
549
+ # Requeue on error
550
+ self.nack(msg, requeue=True)
551
+
552
+ # Check for reply (request/reply pattern)
553
+ if msg.reply_to and msg.reply_to in self._reply_queues:
554
+ # The handler should have put a reply in the reply queue
555
+ pass
556
+
557
+ # Small sleep to prevent busy-waiting
558
+ time.sleep(0.01)
559
+
560
+ except Exception as e:
561
+ print(f"[MemoryBroker] Worker error: {e}")
562
+ time.sleep(0.1)
563
+
564
+ def reply(self, original_message: Message, response: Message) -> None:
565
+ """
566
+ Send a reply to a request message.
567
+
568
+ Used by consumers to respond in request/reply pattern.
569
+
570
+ Args:
571
+ original_message: The request message
572
+ response: The response message
573
+ """
574
+ if not original_message.reply_to:
575
+ raise PublishError("Original message has no reply_to queue")
576
+
577
+ response.correlation_id = original_message.correlation_id
578
+
579
+ with self._lock:
580
+ reply_queue = self._reply_queues.get(original_message.reply_to)
581
+ if reply_queue:
582
+ reply_queue.put(response)
583
+
584
+ # ========================================
585
+ # CLI Helper Methods
586
+ # ========================================
587
+
588
+ def list_queues(self) -> List[Dict[str, Any]]:
589
+ """List all queues with statistics."""
590
+ with self._lock:
591
+ return [
592
+ {
593
+ 'name': name,
594
+ 'messages': q.messages.qsize(),
595
+ 'consumers': len(q.consumers),
596
+ 'durable': q.durable,
597
+ }
598
+ for name, q in self._queues.items()
599
+ ]
600
+
601
+ def list_topics(self) -> List[str]:
602
+ """List all subscribed topics."""
603
+ with self._lock:
604
+ return list(set(
605
+ s.pattern for s in self._subscriptions.values()
606
+ if not s.is_queue and s.active
607
+ ))
608
+
609
+ def peek_queue(self, name: str, limit: int = 10) -> List[Message]:
610
+ """
611
+ Peek at messages in a queue without consuming them.
612
+
613
+ Args:
614
+ name: Queue name
615
+ limit: Maximum messages to return
616
+
617
+ Returns:
618
+ List of messages
619
+ """
620
+ with self._lock:
621
+ if name not in self._queues:
622
+ return []
623
+
624
+ q = self._queues[name]
625
+ messages = []
626
+
627
+ # Convert queue to list temporarily
628
+ temp_list = []
629
+ while not q.messages.empty() and len(temp_list) < 1000:
630
+ try:
631
+ temp_list.append(q.messages.get_nowait())
632
+ except queue.Empty:
633
+ break
634
+
635
+ # Get first N messages
636
+ messages = temp_list[:limit]
637
+
638
+ # Put all messages back
639
+ for msg in temp_list:
640
+ q.messages.put(msg)
641
+
642
+ return messages
643
+
644
+ def delete_queue(self, name: str, force: bool = False) -> None:
645
+ """
646
+ Delete a queue.
647
+
648
+ Args:
649
+ name: Queue name
650
+ force: Force delete even if not empty
651
+ """
652
+ with self._lock:
653
+ if name in self._queues:
654
+ q = self._queues[name]
655
+ if not force and not q.messages.empty():
656
+ raise QueueError(f"Queue '{name}' is not empty. Use force=True to delete anyway.")
657
+ del self._queues[name]
658
+
659
+ def cancel_consumer(self, consumer_id: str) -> None:
660
+ """Cancel a consumer. Alias for unsubscribe."""
661
+ self.unsubscribe(consumer_id)
662
+
663
+ def get_stats(self) -> Dict[str, Any]:
664
+ """Get broker statistics."""
665
+ with self._lock:
666
+ total_messages = sum(q.messages.qsize() for q in self._queues.values())
667
+ total_consumers = sum(len(q.consumers) for q in self._queues.values())
668
+
669
+ return {
670
+ 'broker_type': 'memory',
671
+ 'connected': self._connected,
672
+ 'queues': len(self._queues),
673
+ 'subscriptions': len(self._subscriptions),
674
+ 'total_messages': total_messages,
675
+ 'total_consumers': total_consumers,
676
+ 'pending_acks': len(self._pending_acks),
677
+ }
678
+
679
+ def get_info(self) -> Dict[str, Any]:
680
+ """Get broker connection info."""
681
+ return {
682
+ 'type': 'InMemory',
683
+ 'connected': self._connected,
684
+ 'persistent': False,
685
+ 'note': 'Messages are lost on restart',
686
+ }
687
+
688
+
689
+ # Alias for backwards compatibility
690
+ InMemoryAdapter = MemoryAdapter